RTC (ura realnega časa) za ATMEGA328 in podobne

programski jeziki in programiranje

Moderator: tilz0R

RTC (ura realnega časa) za ATMEGA328 in podobne

OdgovorNapisal/-a oobuco » 09 Mar 2017, 14:15

Ker sem imel sam, sicer že pred časom, precej težav s tem, da sem prišel do tega, da mi ura dela pravilno v vseh pogojih, sem raje kar napisal neke vrste navodila, oziroma bolj smernice, morda, vsaj začetnikom, pride komu kdaj prav.



Če želite privarčevati kakšen euro pri tiskanini in zunanjemu RTC čipu ali pa vsaj pri prostoru, lahko praktično isto izvedete recimo na ATMEGA328 tudi programsko.

Je pa treba pri pisanju upoštevati nekaj pravil, ki morda niso samoumevna, vsaj meni na začetku niso bila.

Kar se hardware-a tiče, rabite kristal 32768Hz, ki ga priklopite na ustrezne pine na mikrokontrolerju.
Večinoma ne rabite nobenih dodatnih kondenzatorjev, je pa to verjetno odvisno od kristala.

Seveda za svoje delovanje mikrokontroler v tem primeru uporablja "internal clock" - do 8MHz, pri 1.8V pa manj kot 4MHz.
Lahko pa s pomočjo priklopljenega kristala "internal clock" tudi kalibrirate na +-1% napake, tovarniško je +-10% napake - OSCCAL.

Ker ima taka ura smisel samo, če je v primeru izpada napajanja mikrokontroler napajan z baterijo, boste rabili še baterijo, recimo 3V litijevo CR2032 in malce dodatnega vezja.

Sicer za namen preklapljanja napajanja iz glavnega na baterijsko in nazaj lahko uporabite posebne čipe ali pa vezavo z mosfeti, vendar so običajno dovolj dobre tudi diode, schotkyeve seveda, da imate čim manjši padec napetosti - cca 0.3V.


Preko prve diode pripeljete recimo 5V glavno napajanje (VCC_Main) na napajanje mikrokontrolerja (VCC_uC), preko druge diode pa napajanje z baterije (VCC_Bat) spet na VCC_uC.
Tako bo baterija prevzela napajanje, kadar bo izpadlo glavno napajanje, ne bo pa se praznila, kadar bo glavno napajanje prisotno.
VCC_uC bo tako sicer imel 0.3V manjšo napetost kot VCC_Main in tudi kot VCC_Bat, vendar je to običajno zanemarljivo.
Mikrokontrolerju lahko privoščite tudi kakšen kondenzator na VCC_uC.
Samo za primer, 1000uF ima dovolj energije, da ura deluje še kakšnih 5 minut po izpadu VCC_Main, če ni baterije seveda.


VCC_Main potem preko dveh zaporedno vezanih običajnih diod in še zaporednega upora 470E (skupaj 1.4V padca napetosti) pripeljite na pin mikrokontrolerja, ki bo služil za kontrolo izpada VCC_Main.
Diodi sta namenjeni temu, da mikrokontroler zazna izpad napetosti čim prej, ne šele ko pade na 3V, ko napajanje prevzame baterija. Upor pa je za zaščito pina, če se programsko zgodi, da postane izhod.
Da od takrat, ko napetost VCC_Main pade, pa do takrat, ko mikrokontroler zaspi, mine čim manj časa, je pametno, da padec napetosti VCC_Main sproži kar prekinitev, zato izberite za ta namen primeren pin.
Pa na pin dajte še 10K pull-down upor.
Pinu bomo v nadaljevanju rekli ExternalPowerSense, prekinitvi pa External_Power_Down.

Napajanje za porabnike, predvsem večje, tudi če so to samo LED diode, je če je to možno, bolje jemati iz VCC_Main kot pa iz VCC_uC.
Tako se ob izpadu VCC_Main niti za trenutek ne napajajo iz baterije.
Morebitna razlika med VCC_uC in napetostjo na pinih, ki bi prišla iz VCC_Main, bo še vedno v mejah dovoljenega - 0.3V, dovoljeno je 0.6V razlike.



Software je na začetku čisto enostaven.

Za začetek boste rabili samo prekinitveno rutino (Clock_1_sek), ki jo bo na eno sekundo prožil TIMER2.
V njej lahko zgolj štejete sekunde ali pa naredite kar pravi koledar z meseci in prestopnimi leti - ampak za začetek bodo sekunde zadoščale.
Uporabljena mora biti prekinitev TIMER2 OVF.
Timer naj deluje kot timer v najbolj običajnem načinu, kjer samo šteje od 0 do 255 in potem preskoči na 0, pri tem prelivu (overflow) pa sproži prekinitev.
TIMER2 mora biti nastavljen tako, da deluje z zunanjim kristalom (asinhroni način), register ASSR.AS2 = 1.
Nastaviti bo treba tudi preskaler TCCR2B.CS22:0, za prekinitev na eno sekundo mora frekvenco kristala deliti s 128, ker pri 128*256 je delitelj ravno 32768, torej prekinitev na eno sekundo.


Ko pa zmanjka elektrike, se malo bolj zakomplicira.

Princip delovanja je, da ko pade VCC_Main napetost, naj gre mikrokontroler v spanje, zbuja pa naj ga na eno sekundo TIMER2 OVF prekinitev.
Takoj, ko prekinitvena rutina preračuna novo stanje sekund, pa naj mikrokontroler spet gre v spanje.
Na ta način mikrokontoler vsako sekundo deluje samo zelo kratek čas - izračun koledarja pač čimbolj optimizirajte - in tako v povprečju rabi samo kakšnih 10uA toka.
Če rečemo, da ima CR2032 med 3V in 2V kakšnih 100mAH, bi bilo to 10000 ur oziroma dobro leto brez glavnega napajanja.
Moja meritev je bila okoli 3uA in pa 6uA z vklopljenim Watchdog-om.


Rabili boste 1 byt prostora v SRAM-u za začasno shranjevanje enega od registrov, spremenljivki recimo R16_store.
Potem boste rabili še en byte Sleep_Flags in notri dva bita, Sleep_flags.GoToSleep in Sleep_flags.NowSleeping, ki naj imata začetno vrednost 0.

Ne glede na to, v kakšnem programskem jeziku pišete, je zaželjeno, da vam dovoli nadzor nad skladom (SP) pri vstopu in izstopu iz prekinitvene rutine, ki jo proži TIMER2 OVF.

Vstop v rutino je vedno isti, izstopi pa so trije različni.

Namreč, ob prisotnem glavnem napajanju je prvi način, ki je enostaven in najbolj običajen.
Ko rutina prvič izstopa v spanje je drugi način.
Tretji način pa je, ko rutina ne izstopa v spanje prvič.


Rutina bi izgledala nekako takole:


Clock_1_sek:
'ker se ta prekinitev zgodi samo vsako sekundo, utegne pa trajati kar nekaj ciklov,
'vendar še vedno veliko, veliko manj kot 1 sekundo, čim prej omogočimo ostale prekinitve
SEI

'najprej pospravimo na sklad R16 in SREG
PUSH R16
IN SREG, R16
PUSH R16

'sicer zadeva deluje, če SMCR register samo enkrat na začetku programa nastavimo na željeni način spanja, ampak priporočilo v dokumentaciji je
'da naj to storimo čim bliže vstopu v spanje, čim prej po zbujanju pa spanje onemogočimo na način, ki sledi
CLR R16
OUT SMCR, R16

PUSH R0
PUSH R1
.
.
.
'naredite toliko pushov kolikor jih pač rabite - s tem nisem mislil sklec!


'-----------------------------------------------------------------
'tu sedaj štejete sekunde ali pa tudi dneve, mesece in leta .....

'to prekinitev lahko uporabite tudi za več timerjev z resolucijo ene sekunde, vendar naj se te rutine
'načeloma ne izvajajo, ko je mikrokontroler v spanju, se pravi ko je Sleep_Flags.NowSleeping = 1

'tu je potem še ena zelo pomembna zadeva, namreč zbujanje
'deluje naj samo, ko je Sleep_Flags.NowSleeping = 1
'vsako sekundo preverjajte ali je na ExternalPowerSense logična 1
'ko to velja po nekaj zaporednih sekundah, postavite Reset_Flags.NowSleeping = 0 in spet omogočite prekinitve za External_Power_Down.
'Potem vklopite vse porte, prekinitve, timerje, naprave, PRR, ...., ki ste jih ugasnili, ko je zmanjkalo elektrike.
'-------------------------------------------------------------------


in sedaj izstop, spet vrnite točno to, kar ste prej pospravili
.
.
.
POP R1
POP R0

'ker je na vrsti odločanje, za kateri način gre, je bolje, da so delček časa vse prekinitve onemogočene
CLI
LDS R16, {Sleep_Flags}
SBRC R16, NowSleeping
RJMP Sleeping
SEI

'če mikrokontroler ni v spanju, izstopite normalno
POP R16
OUT SREG, R16
POP R16
RETI



Sleeping:
'preverite, če je prekinitev pognana zdaj prvič, ko je šel mikrokontroler v spanje
SBRC R16, GoToSleep
RJMP Enter_Sleeping

POP R16
OUT SREG, R16
POP R16

'če ni prvič, izbrišite povratni naslov iz sklada, drugače se vam bo, ko bo mikrokontroler v spanju, sklad podaljševal z vsakim klicem te prekinitve
STS {R16_Store}, R16
POP R16
POP R16

'omogočite "Sleep" in izberite "Power save" način spanja
LDI R16, 7
OUT SMCR, R16

LDS R16, {R16_Store}

'omogočite prekinitve, ker drugače se naslednje bujenje ne bo zgodilo in potisnite mikrokontroler v spanje
SEI
SLEEP
'do tukaj program načeloma nikoli ne pride, ker je takoj, ko se mikrokontroler zbudi, klican na začetek te prekinitve in tukaj spet zaspi
'zato se sklad podaljšuje, kar ni pravilno
'ampak vseeno, že zaradi higiene naj bo tukaj tudi return
RETI



Enter_Sleeping:
'ugasnemo flag, da velja samo enkrat, prvič
CBR R16, GoToSleep
STS {Sleep_Flags}, R16

POP R16
OUT SREG, R16
POP R16

STS {R16_Store}, R16

'omogočite "Sleep" in izberite "Power save" način spanja
LDI R16, 7
OUT SMCR, R16

LDS R16, {R16_Store}

SEI
SLEEP
'v tem načinu smo vseeno ohranili prvi povratni naslov, kamor se bo mikrokontroler vrnil, ko se bo povrnilo napajanje VCC_Main

RETI





Manjka nam samo še prekinitvena rutina External_Power_Down, ki se sproži, ko pade VCC_Main.

Najbolj primerni za to sta INT0 in INT1 prekinitvi, to pa zato, ker lahko vnaprej določite, da naj se prožita zgolj ob prehodu stanja na pinu ExternalPowerSense iz 1 na 0.

PCINT prekinitve pa se prožijo pri obeh prehodih, se pravi iz 1 v 0 in iz 0 v 1.
Torej, če uporabljate PCINT je potrebno narediti še dvoje.
Čim prej na vhodu v External_Power_Down preverite trenutno stanje ExternalPowerSense in če je 1 potem prekinitev ignorirajte, oziroma čimprej izstopite iz nje.
Prav tako, ko se povrne VCC_Main in v Clock_1_sek spet omogočite to prekinitev, prej pobrišite ustrezen flag v PCIFR registru, ki kaže, da je prišlo do zahteve za prekinitev, ker se je ExternalPowerSense tačas spremenil iz 0 v 1.
Morda boste pri PCINT rabili tudi manjši kondenzator (100nF) vzporedno z 10K uporom na ExternalPowerSense pinu.
Z njim boste filtrirali morebitne zelo kratke izpade VCC_Main, boste pa zato izpad zaznali malce kasneje, odvisno od RC konstante.


Ko se sproži External_Power_Down prekinitev, morajo biti vse prekinitve onemogočene.

Zdaj morate čimprej minimizirati porabo.
Najprej vse porte postavite na input brez internega pull-up.
Ne pozabite, da so mogoče kateri od portov zasedeni z drugimi napravami, recimo porta RX in TX za serijsko komunikacijo.
To morate najprej ugasniti, recimo za RX in TX v registru UCSR0B bita 3 in 4 postavite na 0, se zelo pozna na porabi.
Tistim portom, ki so v zraku, je pametno vključiti interni pull_up.
Za ExternalPowerSense port interni pull_up ne smete vključiti, ker že ima zunanji pull_down in bi tako lahko odtekal tok.

Potem s pomočjo PRR registra ustavite vse, razen TIMER2, PRR=191.

Nato posamično onemogočite vse prekinitve, ki jih uporabljate, razen seveda TIMER2 OVF.
Med spanjem namreč ni dobro, da se izvede kakšna druga prekinitev kot samo TIMER2 OVF, ker bi to še dodatno zapletlo kodo.

Postavite bita Sleep_flags.GoToSleep in Sleep_flags.NowSleeping na 1 in potisnite mikrokontroler v spanje.

Če uporabljate WatchDog, potem ga ne resetirajte v prekinitveni rutini Clock_1_sek, ker to ne bi imelo pravega smisla.
Torej ga tudi ugasnite, preden potisnete mikrokontroler v spanje.

Če pa ga vseeno želite uporabljati tudi med spanjem, se pravi v prekinitveni rutini Clock_1_sek, potem mora biti seveda Watchdog timeout daljši od ene sekunde.
Je pa res, da bo potem poraba večja.


Če niste prepričani, da ste vse naredili za to, da bo poraba minimalna, lahko to izmerite recimo z uCurrent inštrumentom.

Je pa še drugi način.
Na VCC_uC dajte elektrolitski kondenzator 1000uF in odstranite baterijo.
Z meritvijo napetosti na VCC_uC glede na čas in malo vaje z nabojem v kondenzatorju, boste lahko izračunali kakšen tok teče med spanjem.
Prav tako boste lahko izmerili koliko ste uspešni s časom prehajanja mikrokontrolerja v stanje spanja.
Takoj po odklopu glavnega napajanja bo namreč napetost VCC_uC hitro padla na kakšne 4V, potem pa bo lahko padala do 2V tudi 5 minut.


Pa še nekaj glede SRAM-a, oziroma prostora, kjer so shranjene spremenljivke s tekočimi sekundami, minutami, ...., letom.

Mikrokontroler ob praktično kateri koli vrsti reseta ne pobriše vsebine SRAM-a.
Pač pa to običajno naredijo kar prevajalniki pri inicializaciji mikrokontrolerja.
Vsaj v BASCOM AVR se temu izognete z direktivo $noramclear, vrednost MCUSR registra pa pri vstopu v program dobite v R0.

Vrste reseta so PowerOnReset, ExternalReset, WatchdogReset in BrownOutReset.
V določenih primerih ostane vsebina neokrnjena tudi ob PowerOnReset.
Če recimo spustimo napetost na VCC_uC nekje na 1.3V, potem se bo mikrokontroler ustavil in bo v reset stanju, vendar ne bo spremenil vsebine SRAM-a.
Ko spet dvignemo napetost preko 1.5V mikrokontroler izvede PowerOnReset a vsebina SRAM-a ostane ohranjena.

Tako recimo lahko ostane nastavitev ure neokrnjena kljub namernim ali nenamernim reset-om.
Kakšna sekunda bo seveda manjkala.
Lahko pa recimo naredimo tako, da uro resetiramo samo, ko gre za PowerOnReset - register MCUSR.

Velikokrat je tudi bolje pisati v SRAM, kot v EEPROM, še posebej, če gre za kakšne loge, recimo števec Watchdog resetov z namenom debugiranja ali kaj podobnega.
Pisanje v EEPROM je pač mnogo počasneje in tudi število pisanj je omejeno.




Pa na koncu še glede točnost ure.

Točnost ure bo v navečji meri pač odvisna od kvalitete kristala in njegove občutljiosti na spremembo napetosti in temperature.
Z malim trikom in precej vaje pa lahko točnost povečate, tako da preverjate napetost VCC_uC in interno temperaturo mikrokontrolerja.

Oboje lahko dobite z ADC meritvijo in sicer na kanalu ADC8 dobite temperaturo (merite z interno referenco 1.1V),
na kanalu ADC14 pa lahko izmerite BandGap napetost, ki je za ATMEGA328 1.1V.
ADC14 merite z referenco VCC, ki je dejansko VCC_uC in ker veste, da bi moralo pri 5V biti 1.1V potem z malo računanja lahko ugotovite kolikšen je dejansko VCC.

Potem pa glede na izmerjeno, vsake toliko časa kakšno sekundo dodate ali odvzamete.
Lahko pa tudi spreminjate register TCNT2 - hitro preberete in povečate ali zmanjšate za kakšno 1/256 sekunde, časa imate slabe 4ms.

Na ta način preko OSCCAL lahko seveda tudi povečate točnost za "internal clock".
S pomočjo kristala, ki sicer služi uri in pa OSCCAL pa lahko "internal clock" tudi zamaknete in imate lahko precej točen kakšen od hitrejših Baud Rate-ov.




Vse zgoraj opisano bo verjetno precej težko izvesti v Arduino okolju, ne bo pa najbrž težav z AVR-Studio in tudi ne z BASCOM AVR, kjer se lahko Basic in zbirnik brez težav mešata.


Verjetno obstaja tudi kakšen drugačen pristop in če ga kdo ve, bi bil vesel, če ga obelodani tu na forumu.
Pred EUR sem bil SIT! (sposojeno z grafita)
Uporabniški avatar
oobuco
 
Prispevkov: 56
Pridružen: 19 Jan 2015, 14:05
Kraj: Šentvid pri Stični
Zahvalil se je: 23 krat
Prejel zahvalo: 17 krat
Uporabnika povabil: VolkD
Število neizkoriščenih povabil: 6

Vrni se na Software

Kdo je na strani

Po forumu brska: 0 registriranih uporabnikov in 1 gost