SPI

Über den SPI-Bus wer­den so­wohl die Mess­wer­te der Sen­ke ab­ge­ru­fen als auch der Soll­wert über den DAC ein­ge­stellt.

Der SPI_STC-In­ter­rupt (Serial Trans­fer Com­plete) über­trägt pau­sen­los und kon­ti­nu­ier­lich den Sen­de­puf­fer und schreibt die Emp­fan­ge­nen Da­ten in den Emp­fangs­puf­fer.

Bei 7.3728 MHz wird er mit ei­ner Fre­quenz von ca. 12.2 kHz auf­ge­ru­fen und be­nö­tigt et­wa 900 µs um den kom­plet­ten Puf­fer zu sen­den.

Die­se Zeit be­stimmt auch die ma­xi­ma­le Re­ak­tions­zeit auf kri­ti­sche Er­eig­nis­se (SOA!). Es kann schlimms­ten­falls 1.8 ms dau­ern bis Eload da­r­auf re­agiert.

Timer Interrupt

Für al­le zeit­ge­steu­er­ten Ak­tio­nen löst Timer 0 mit ei­ner Fre­quenz von 800 Hz ei­nen In­ter­rupt aus.

An­fäng­lich hat­te ich hier nur 100 Hz vor­ge­se­hen doch es er­wies sich als zu lang­sam für den En­co­der. Die­ser hat sei­ne Schalt­flan­ken näm­lich, aus mir un­ver­ständ­li­chen Grün­den, nicht zwi­schen den Steps, son­dern wäh­rend­des­sen und er­zeugt da­mit wäh­rend ei­nes Klicks zwei Flan­ken. Mit 10 ms zwi­schen den Ab­tas­tun­gen ist das nicht zu­ver­läs­sig mach­bar.

Wa­rum ich nicht den Pin Change In­ter­rupt be­nutzt ha­be se­hen sie hier.

Tastatur

Die Tas­ta­tur wird über ei­ne Call­back-Funk­tion al­le 10 ms per Timer-In­ter­rupt ge­scannt. Die 10 ms be­stim­men gleich­zei­tig die Ent­prell­zeit für die Tas­ten­drü­cke (10ms×Anzahl_der_Scan­lines).

Für den En­co­der reicht das nicht, die­ser wird al­le 1.25 ms ab­ge­tas­tet um kei­nen Step zu ver­pas­sen.

Jeder Tas­ten­druck und je­de En­co­der-Ak­tion wird in ei­nen Puf­fer ge­schrie­ben und kann vom Haupt­pro­gramm mit­tels getKey() ab­ge­fragt wer­den, was die­sen Puf­fer Zei­chen für Zei­chen wie­der leert. En­co­der-Ak­tio­nen wer­den durch die Spe­zial-Codes KEY_ROTUP und KEY_ROTDOWN über­mit­telt. Ein Druck des En­co­der-But­tons wird als KEY_ENTER über­mit­telt könn­te aber na­tür­lich bei Bedarf auch ei­nen ei­ge­nen Code aus­lö­sen. Im Mo­ment ist es je­doch nur sinn­voll, dass der En­co­der-But­ton die glei­che Ak­tion aus­löst wie auch die <En­ter>-Tas­te (#) des Key­boards.

Menüs

Ein simp­les zei­len­orientier­tes Me­nü wur­de im­ple­men­tiert. Um auch mehr als drei Me­nü­ein­trä­ge zu er­lau­ben (die Ti­tel­leis­te und das ei­gent­li­che Me­nü) wur­de es scroll­fähig ge­stal­tet. Ein Me­nü kann da­mit bis zu 127 Ein­trä­ge um­fas­sen. Das wä­re na­tür­lich als UI un­taug­lich aber mehr als drei sind schon ab und zu nö­tig... An­wäh­len ei­nes Me­nü­punk­tes ruft ei­ne ent­spre­chen­de Call­back-Funk­tion auf wel­che die ent­spre­chen­de Funk­tio­na­li­tät re­a­li­siert. Wenn die Call­back-Funk­tion NULL ist wird das Me­nü be­en­det und kehrt zur auf­ru­fen­den Funk­tion zu­rück. Das Haupt­me­nü soll­te das nicht tun, wenn­gleich die Haupt­schlei­fe in die­sem Falle das Haupt­me­nü er­neut star­ten wür­de aber das wä­re nur be­las­tend für den User.
Der Auf­ruf ei­nes Me­nüs hält das auf­ru­fen­de Pro­gramm bis zu sei­nem En­de an. Flags (wie Timer- oder Kom­mu­ni­ka­ti­ons­flags) wer­den fort­an in­ner­halb der Me­nü-Schlei­fe ab­ge­han­delt.

Ein Me­nü­punkt star­tet i.d.R. ei­nen Screen oder ein wei­te­res (Sub-) Me­nü, der/das sei­ner­seits die Pro­gramm­aus­füh­rung bis zu sei­nem En­de an­hält. Er/es ist in die­ser Zeit eben­falls für die Be­ar­bei­tung der Flags ver­ant­wort­lich.

Die Rou­ti­ne doFlags wird da­zu nach je­dem Sleep auf­ge­ru­fen und er­le­digt al­les nö­ti­ge.

Son­der­ak­tio­nen wie z.B. die Ka­li­brie­rung, der Fer­ti­gungs­test oder die Pro­gram­mie­rung von Zeit­kon­stan­ten kön­nen nur über den USB-An­schluss durch­ge­führt wer­den. Na­tür­lich könn­te auch ein ent­spre­chen­des Me­nü ent­wor­fen wer­den aber we­gen der Sel­ten­heit die­ser Er­eig­nis­se wä­re das nur Ver­schwen­dung von Res­sour­cen.

PROGMEM

Soll dass Me­nü samt sei­ner Kom­po­nen­ten (der Me­nü­ein­trä­ge) im Flash oder im RAM ge­hal­ten wer­den?
Es gibt Ar­gu­men­te für bei­des. Ein Me­nü im RAM kann vom Haupt­pro­gramm je­der­zeit an­ge­passt, um zu­sätz­li­che Ein­trä­ge er­gänzt oder um nicht sinn­vol­le Op­tio­nen ge­kürzt wer­den. Aller­dings ist der Spei­cher­ver­brauch be­trächt­lich, wenn un­ser Sys­tem nur zwei Ki­lo­bytes RAM be­sitzt.

Ich ha­be das Me­nü so pro­gram­miert, dass es kom­plett im Flash-Spei­cher ge­hal­ten wird. Dy­na­misch er­schei­nen­de oder ver­schwin­den­de Ein­trä­ge hal­te ich oh­ne­hin für eher ir­ri­tie­rend (in der Hil­fe steht, drü­cken sie .. um et­was zu tun, aber wa­r­um .. nicht er­scheint oder aus­ge­graut ist sagt sie nicht).

Screens

Die Be­dien­ober­flä­che ist in ein­zel­ne Bild­schir­me (Screens), auf­ge­ru­fen durch ein Me­nü oder auch di­rekt durch ei­nen an­de­ren Screen, auf­ge­teilt.

Wenn ein Screen auf­ge­ru­fen wird, hält er die Haupt­schlei­fe bis zu sei­nem En­de an. Die Haupt­schlei­fe re­du­ziert sich des­halb auf ei­ne Rei­he von Screens oder Sub­me­nues, die nach­ein­an­der auf­ge­ru­fen wer­den, evtl. ab­hän­gig vom Rück­ga­be­wert des je­weils vor­he­ri­gen.

Ein Screen wird nor­ma­ler­wei­se durch Drü­cken des En­ter- oder Cancel- But­tons durch den Be­nut­zer be­en­det. Es kann aber auch ein Time­out vor­ge­ge­ben wer­den, wie es z.B. beim Be­grü­ßungs­bild­schirm sinn­voll ist.

Es ist da­her nicht ga­ran­tiert, dass die Haupt­schlei­fe in­ner­halb ei­ner be­stimm­ten Zeit durch­läuft. Tat­säch­lich läuft die Haupt­schlei­fe im Ide­al­fall über­haupt nicht durch son­dern stoppt mit dem Auf­ruf des Haupt­me­nüs, das nie­mals be­en­det wer­den soll­te.

Flags wie z.B. das Se­kun­den­flag oder Kom­mu­ni­ka­ti­ons­flags wer­den in­ner­halb des Screens ab­ge­han­delt.

Ein Screen ent­hält Wid­gets wie Texte, But­tons oder Ein­ga­be­fel­der.

Die­se sind in ei­ner C-Struct zu­sam­men­ge­fasst. Jedes die­ser Wid­gets wird nach­ein­an­der auf das Dis­play ge­schrie­ben, was nor­ma­ler­wei­se nur ein ein­zi­ges Mal nö­tig wä­re.

Ein Au­to­re­fresh-Widget wur­de ein­ge­führt um bei­spiels­wei­se Mess­wer­te an­zu­zei­gen, die sich auch oh­ne User-In­ter­ven­tion än­dern kön­nen. Die­se wer­den et­wa drei­mal pro Se­kun­de ak­tu­a­li­siert. Die­se Fre­quenz kann der Be­nut­zer op­tisch noch er­fas­sen und er­mög­licht doch ra­sche Re­ak­tio­nen auf un­er­war­te­te Än­de­run­gen. We­sent­lich hö­he­re Fre­quen­zen wür­den nur als stö­ren­des Flim­mern oh­ne zu­sätz­li­chen In­for­ma­tions­ge­halt wahr­ge­nom­men, we­sent­lich nied­ri­ge­re wür­den die Re­ak­tions­fä­hig­keit ver­rin­gern und den Be­nut­zer un­ge­dul­dig wer­den las­sen.

2..3 Hz sind da­für ein gu­ter Wert. Falls Ihr Gerät un­ter nied­ri­gen Um­ge­bungs­tem­pe­ra­tu­ren ar­bei­ten muss evtl. auch we­ni­ger, da das Dis­play dann trä­ger wird und ra­sche Än­de­run­gen nicht les­bar ab­bil­den kann.

Screen Ma­nu­al Test

Die­ser Screen dient zum ma­nu­el­len Test der Quel­le un­ter kon­stan­ter Last. Der Strom kann über die Tas­ta­tur ein­ge­ge­ben wer­den und/oder durch Drehen des En­co­ders.

Set Point: 1.2340A
1.234A 12.345V
15.23W 10.00Ω
TIC=27.4 THS:78.9 <

Set Point ist da­bei der ein­ge­stell­te Soll­wert. In der Zei­le da­r­un­ter wird der tat­säch­­li­che Strom und die an­lie­gen­de Span­nung an­ge­zeigt. Da­run­ter se­hen Sie die da­r­aus be­rech­ne­te Ver­lust­leis­tung der Sen­ke (nicht not­wen­di­ger­wei­se der Quel­le wenn da noch ohm­sche An­tei­le vor­han­den sind!) so­wie um­ge­rech­net, den Wi­der­stand den die Quel­le sieht. Darin ent­hal­ten ist auch der Leis­tungs­ver­lust in der Si­che­rung da der Span­nungs­ab­fall über die­ser mit ge­mes­sen wird. Un­be­rück­sich­tigt bleibt, da un­be­kannt, die Leis­tung über der rest­li­chen Ver­ka­be­lung, so dass die Strom­quel­le tat­säch­­lich et­was mehr Leis­tung ein­speist. In der drit­ten Zei­le se­hen wir die Tem­pe­ra­tur des Kühl­kör­pers (THS=Temp Heat Sink) so­wie die Tem­pe­ra­tur im Ge­häu­se (TIC=Temp Inside Case). Das <-Zei­chen ist der (ab­ge­kürz­te) Back-But­ton mit dem die­ser Screen be­en­det wer­den kann.

In die­sem Screen kann, so­fern nicht ge­ra­de das Set Point-Widget be­ar­bei­tet wird, je­der­zeit durch Drü­cken von 0..9 ei­ner der zehn Pre­sets auf­ge­ru­fen wer­den oder, falls wir uns im Set Point Widget be­fin­den, der Strom durch Drehen des In­kre­men­tal-Ge­bers in Schrit­ten der ak­tu­el­len Cur­sor­po­si­tion er­höht oder ver­rin­gert wer­den.

Der Preset 0 ist auf 0 mA ge­setzt und kann nicht ver­än­dert wer­den. So ist es stets mög­lich, den Strom durch drü­cken der 0-Tas­te schnell ab­zu­schal­ten falls die Si­tu­a­tion dies er­for­dert.

Ein lan­ger Druck ei­ner Zif­fern­tas­te (au­ßer 0) spei­chert den ak­tu­el­len Strom als den ent­spre­chen­den Preset.

Die­ser Screen eig­net sich sehr gut um die Strom­quel­le an ih­ren Gren­zen oder mit­tels der Pre­sets ge­gen Last­sprün­ge zu tes­ten.

Einstellen der Presets

Die zehn Pre­sets kön­nen über die USB-Schnitt­stel­le per Kom­man­do di­rekt in mA ein­ge­stellt wer­den. Al­ter­na­tiv kön­nen Sie auch in der Stan­dard-An­zei­ge durch lan­ges Drü­cken ei­ner Zif­fern­tas­te auf den ak­tu­ell ein­ge­stell­ten Wert ge­setzt wer­den.

Sie wer­den dann di­rekt im EEPROM ver­ewigt und ste­hen auch nach ei­nem Neu­start des Ge­räts zur Ver­fü­gung.

Status-Anzeige

Die Sta­tus-An­zei­ge sieht so aus:

1.234A 12.345V
15.23W 10.00Ω
THS=78.9°C TIC=23.4°C
P12=11.3V P5=4.93V

Auch hier se­hen wir die an­lie­gen­de Span­nung, den flie­ßen­den Strom, die Ver­lust­leis­tung in der Sen­ke so­wie den äqui­va­len­ten Wi­der­stand, den die Sen­ke für die Strom­quel­le dar­stellt. Zu­sätz­lich se­hen wir die Tem­pe­ra­tu­ren am Kühl­kör­per so­wie im Ge­häu­se und au­ßer­dem die Span­nun­gen P12 und VCC (P5)

Der Stat-Screen wird durch Drü­cken ei­ner be­lie­bi­gen Tas­te be­en­det und führt zu­rück zum Me­nü.

Eingabefelder

Ein­ga­be­fel­der er­lau­ben die Ein­ga­be von nu­me­ri­schen Wer­ten über die Tas­ta­tur und/oder den En­co­der. Ein Ein­ga­be­feld wird durch die En­ter-Tas­te be­tre­ten und kann dann über die Zif­fern­tas­ta­tur oder den En­co­der ver­än­dert wer­den.

Das bringt uns in ei­ne Zwick­müh­le. Ei­ner­seits sol­len Ver­tip­per kei­ne un­ge­woll­te Än­de­rung des Stro­mes be­wir­ken, an­de­rer­seits soll mit dem En­co­der in Echt­zeit ei­ne Fein­jus­ta­ge des Stro­mes mög­lich sein. Was soll al­so pas­sie­ren, wenn der User ein paar Stel­len tippt und dann am En­co­der dreht? Die ak­tu­el­le Soft­ware macht fol­gen­des:

Wenn Zif­fern­tas­ten ge­drückt wer­den, wird der Wert des Fel­des ent­spre­chend ge­än­dert, der tat­säch­­lich flie­ßen­de Strom wird je­doch noch nicht ge­än­dert. Beim Drehen des En­co­ders wird der Wert in der ak­tu­el­len Cur­sor­po­si­tion um eins er­höht/er­nied­rigt und der Strom ent­spre­chend ein­ge­stellt, selbst wenn der Wert vor­her durch Zif­fern-Ein­ga­be ge­än­dert wur­de.

Beim Ver­las­sen des Fel­des nach links (durch Drü­cken von Cancel) wird der Wert vor dem Be­tre­ten wie­der her­ge­stellt und der Strom ent­spre­chend ein­ge­stellt.

Beim Ver­las­sen des Fel­des nach rechts (durch Drü­cken von En­ter) wird der ein­ge­stell­te Wert als Strom ge­setzt.

Das kann durch­aus be­wusst aus­ge­nutzt wer­den um Last­sprün­ge ma­nu­ell zu er­zeu­gen: Stel­len Sie den Strom für das En­de des Last­sprungs zu­nächst ein und ver­las­sen Sie das Feld mit En­ter. Dann be­tre­ten Sie das Feld er­neut und stel­len Sie den Wert für den Be­ginn des Last­sprungs mit dem En­co­der ein. Wenn Sie nun das Feld mit Cancel ver­las­sen wird der Strom schlag­ar­tig auf den ur­sprüng­li­chen Wert zu­rück­ge­stellt und Sie ha­ben ih­ren Last­sprung!

USB

Die USB-Schnitt­stel­le ist na­tür­lich gal­va­nisch vom Rest der Schal­tung iso­liert um Rück­wir­kun­gen und mög­li­cher­wei­se fa­ta­le Schlei­fen­strö­me über den steu­ern­den PC (des­sen Mas­se ja im Re­gel­fall ge­er­det ist) zu ver­hin­dern.

Die Tren­nung ist bis zu ei­nem Wert von 500 V= zwi­schen der Eload-Mas­se und USB-GND ga­ran­tiert.

Be­ach­ten Sie, dass auch Tran­si­en­ten die­sen Wert nicht über­schrei­ten dür­fen! USB-Mas­se und Schal­tungs-Mas­se sind mit 450 kΩ || 2,2 nF mit­ein­an­der ver­bun­den.

Senden über den USB er­folgt klas­sisch über ei­nen Sen­de­puf­fer im Gerät. Bei ein­ge­hen­den Da­ten wer­den die­se ge­sam­melt bis ein LF ein­trifft. Dann wird die ge­sam­te Zei­le als Be­fehl aus­ge­wer­tet. Der Host soll­te kei­ne wei­te­ren Da­ten sen­den bis er die Ant­wort auf die­sen er­hält da sonst Zei­chen ver­lo­ren ge­hen kön­nen.

Entschuldigung :-)

Für den Rest die­ser Sei­te bin ich lei­der aus nicht mehr nach­voll­zieh­ba­ren Grün­den ins Eng­li­sche ge­rutscht. Ich las­se es aber so bis das Eload-Ka­pi­tel ei­nen vor­läu­fig end­gül­ti­gen Zu­stand er­reicht hat. Sorry for any inconvenience...

Interrupts

Interrupts generally are a very sensitive point in designing software. This applies in a very special way to Atmel's AVRs since there is no effective priority mechanism, interrupts can normally not be interrupted, not even by interrupts of higher priority.

Timer Interrupt

The timer interrupt is called at 1.25 ms intervals and in this application has a lot of tasks:

The time spend in the ISR may be considerably long so this routine is the only one that sets the interrupt enable flag right on entry. This interrupt can thus be interrupted by any of the other ones.

One problem when doing this is that it could also be interrupted by itself (i.e. the next timer interrupt before the routine completes) and thereby might cause a stack explosion, but since the timing is predictable (1.25 ms) this can be safely excluded unless its worst case execution time would exceed 1.25 ms. Be aware that this time includes the execution time of the interrupting routines!

Another problem which can cause tricky errors really occured because the timer interrupt (especially the keyboard scanning part) modifies the same port as the spi interrupt. Although both routines modify only the bits they are intended to, the keyboard scan does this in a non-atomic way. I occasionally noticed the leds flashing and got spurious false measurements before I disabled interrupts during the scan...

Some tasks, like scanning the keyboard or setting the LEDs is done in 10 ms intervals.

The 100 Hz part is not done in one bulk routine but I spreaded different tasks to several of the 8 interrupts called every 10 ms. So the worst case execution time is reduced and the CPU load is spreaded more equal over time. The time necessary for the switch() construct is negligible.

And what about the pin change interrupt?

This is available on some devices. It causes an interrupt every time the pin state changes. This is extremely dangerous and I would prefer it would not exist!

At a first glance, it seems to be perfectly useful for e.g. the rotary encoder but mechanical contacts tend to bounce. You do not know how fast and how often. I have witnessed a program collapsing because some comparator put a megahertz where the developer only expected kilohertz. The CPU never came out of the pin change interrupt and seemed to be frozen. Use it only on signals where no higher frequency may appear than you expect!
This may be useful for a hardware debounced key but debouncing it by software is usually cheaper!

SPI Interrupt

The SPI peripheral on ATmegas lacks the cooperation of a DMA unit. This is bad. You either have to handle the SPI inside your main loop what includes waiting for every single byte to be transferred before you can send the next one or you must accept the overhead of an ISR for every single byte and handle the thing via ISR.

If the SPI operates at high clock rates, the first version may be preferrable, at low clock rates the ISR version may consume less CPU power and spread the load more equal over time. In any case, the SPI is a device that annihilates significant CPU power.

I decided to use the second method and to reduce the SPI clock to an acceptable rate.

All SPI devices (the ADC, the DAC and the LED shift register) are handled inside the SPI interrupt. Since our device only has one SPI, this method is less error prone than to handle the ADC via interrupt and create some other routines that set up the DAC and the LEDs which must be sure that the ADC is currently not in use. The ADC in fact could be a little bit faster otherwise. Don't get you into trouble unless you need to...

The SPI interrupt at 7.3728 MHz is triggered roughly every 80 µs and takes worst case 22.8 µs to execute. Read the paragraph Timing Analysis why this is only half of the truth. It can thus consume about one third of the CPU power worst case. In average, it will be less than 20%.

This is the most important task in our circuit so it may do so...

ADC Interrupt

This is the ISR for the on chip ADC of the ATmega. It scans all lines (i.e. temperature and voltage sensors) regularly. Due to the design of the ISR it is essential that no ADC interrupt gets lost, else it would lead to values being transferred to the wrong channel what would be someway catastrophic. No other interrupt must take more time than a single ADC conversion!

USART Interrupt

RxD Interrupt

Every single byte is transferred to the receive buffer and collected until a <LF> is received. Then the complete line is dispatched as a host command and executed by the surrounding loop.

TxD Interrupt

The TxD-interrupt empties the transmit buffer. Bytes are transmitted as soon as possible until the transmit buffer is empty.

Timing analysis

Especially for interrupts it is necessary that you know exactly how much CPU power an interrupt will consume, in average as well as in worst case.

Usually you will do that by setting a port pin right on entry and reset it as a terminal action. With a scope or in the simulator you can measure the time it takes. A simple RC filter can show you the average load and a modern scope can tell you the maximum time it consumes.

But there is one thing you may overlook when doing so: the time the prolog and epilogue code takes! Before you can set the port pin the code has to save the registers it might touch and after you reset the pins, these values will be restored. The total execution time will be, e.g. in our SPI interrupt, about 9 µs longer than you will measure. You can check this out by using the disassembly window in the simulator by triggering on the very first machine inst.

This is more than the average execution time of the interrupt itself!

Optimization

Especially our SPI interrupt beares a great potential of optimization that could even be done by the compiler (but unfortunately it is not):
in 63 out of 64 calls most of the registers that it saves on the stack are not even touched! The compiler could save them just in branches where they were needed!

There is also a possibility of manual optimization. Create a subroutine for the 64th call. This would increase the worst case load (due to the call and its return) but it would decrease the average load due to the less number of registers to save in the other 63 calls.

It depends on your application if this approach is useful. In a real time application it might be better to reduce the worst case execution time, in an application with lots of other tasks running, it might be better to reduce the average load.

If you want it to be perfect, I'm afraid you still would have to write it in assembler code...

In our case, I decided for the minimum worst case execution time since our CPU has not much else to do.

OffTopic: 5x7 LCD Font

abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ

The font I use here for visualizing display content is freely available for download as webfont or ttf. Have a look on my HTML and Javascript page to learn more.