SPI

Über den SPI-Bus werden sowohl die Messwerte der Senke abgerufen als auch der Sollwert über den DAC eingestellt.

Der SPI_STC-Interrupt (Serial Transfer Complete) überträgt pausenlos und kontinuierlich den Sende­puffer und schreibt die Emp­fange­nen Da­ten in den Emp­fangs­puffer.

Bei 7.3728 MHz wird er mit einer Fre­quenz von ca. 12.2 kHz auf­ge­ru­fen und be­nötigt etwa 900 µs um den kompletten Puf­fer zu senden.

Diese Zeit bestimmt auch die maximale Re­ak­tions­zeit auf kri­tische Er­eignis­se (SOA!). Es kann schlimmsten­falls 1.8 ms dauern bis Eload da­rauf re­agiert.

Timer Interrupt

Für alle zeitgesteuerten Ak­tionen löst Timer 0 mit ei­ner Fre­quenz von 800 Hz ei­nen Inter­rupt aus.

Anfänglich hatte ich hier nur 100 Hz vor­ge­se­hen doch es er­wies sich als zu langsam für den En­co­der. Dieser hat seine Schalt­flan­ken näm­lich, aus mir unverständlichen Gründen, nicht zwi­schen den Steps, son­dern wäh­rend­des­sen und er­zeugt damit während eines Klicks zwei Flanken. Mit 10 ms zwischen den Ab­tas­tungen ist das nicht zuver­lässig mach­bar.

Warum ich nicht den Pin Change Interrupt benutzt habe sehen sie hier.

Tastatur

Die Tastatur wird über eine Call­back-Funktion alle 10 ms per Timer-Inter­rupt ge­scannt. Die 10 ms be­stim­men gleich­zeitig die Ent­prell­zeit für die Tasten­drücke (10ms×Anzahl_der_Scan­lines).

Für den Encoder reicht das nicht, dieser wird alle 1.25 ms ab­ge­tas­tet um keinen Step zu ver­pas­sen.

Jeder Tas­ten­druck und je­de En­coder-Aktion wird in einen Puffer ge­schrie­ben und kann vom Haupt­programm mit­tels getKey() abge­fragt wer­den, was die­sen Puf­fer Zei­chen für Zei­chen wie­der leert. Encoder-Aktionen werden durch die Spezial-Codes KEY_ROTUP und KEY_ROTDOWN über­mittelt. Ein Druck des En­co­der-But­tons wird als KEY_ENTER über­mittelt könnte aber natür­lich bei Be­darf auch ei­nen eigenen Code aus­lösen. Im Moment ist es jedoch nur sinn­voll, dass der Encoder-Button die gleiche Ak­tion auslöst wie auch die <Enter>-Taste (#) des Key­boards.

Menüs

Ein simples zeilenorientiertes Menü wurde imple­men­tiert. Um auch mehr als drei Menü­ein­trä­ge zu er­lau­ben (die Ti­tel­leis­te und das ei­gent­liche Menü) wur­de es scroll­fähig ge­stal­tet. Ein Menü kann damit 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 Menü­punk­tes ruft ei­ne ent­spre­chen­de Call­back-Funk­tion auf welche die ent­spre­chen­de Funk­tio­na­li­tät re­ali­siert. Wenn die Callback-Funktion NULL ist wird das Menü be­endet und kehrt zur auf­rufen­den Funktion zurück. Das Haupt­menü soll­te das nicht tun, wenn­gleich die Haupt­schleife in die­sem Falle das Haupt­menü er­neut star­ten wür­de aber das wäre nur be­lastend für den User.
Der Auf­ruf eines Menüs hält das auf­ru­fen­de Pro­gramm bis zu sei­nem Ende an. Flags (wie Timer- oder Kom­munikations­flags) werden fortan inner­halb der Menü-Schleife abge­han­delt.

Ein Menüpunkt startet i.d.R. einen Screen oder ein weiteres (Sub-) Menü, der/das seiner­seits die Programm­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 Bear­beitung der Flags verant­wort­lich.

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

Sonder­aktionen wie z.B. die Kali­brie­rung, der Fer­ti­gungs­test oder die Pro­gram­mierung von Zeitkon­stanten können nur über den USB-An­schluss durch­ge­führt werden. Natürlich könnte auch ein ent­sprechen­des Menü ent­worfen wer­den aber we­gen der Sel­ten­heit die­ser Ereig­nisse wä­re das nur Ver­schwen­dung von Res­sour­cen.

PROGMEM

Soll dass Me­nü samt seiner Kom­po­nen­ten (der Menü­einträ­ge) im Flash oder im RAM ge­hal­ten wer­den?
Es gibt Ar­gumen­te für be­ides. Ein Menü 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­volle Op­tionen ge­kürzt wer­den. Aller­dings ist der Spei­cher­ver­brauch be­trächt­lich, wenn un­ser Sys­tem nur zwei Kilo­bytes RAM be­sitzt.

Ich habe das Menü so pro­gram­miert, dass es kom­plett im Flash-Speicher ge­hal­ten wird. Dyna­misch er­schei­nen­de oder ver­schwin­den­de Ein­trä­ge hal­te ich ohne­hin für eh­er ir­ri­tie­rend (in der Hi­lfe steht, drücken sie .. um et­was zu tun, aber wa­rum .. nicht er­scheint oder aus­ge­graut ist sagt sie nicht).

Screens

Die Bedien­ober­flä­che ist in ein­zelne Bild­schirme (Screens), aufge­rufen durch ein Menü oder auch direkt durch einen anderen Screen, aufge­teilt.

Wenn ein Screen auf­ge­ru­fen wird, hält er die Haupt­schlei­fe bis zu sei­nem Ende an. Die Haupt­schleife redu­ziert sich des­halb auf eine Rei­he von Screens oder Sub­me­nues, die nach­ein­ander auf­ge­ru­fen wer­den, evtl. ab­hängig vom Rück­gabe­wert des je­weils vor­herigen.

Ein Screen wird normaler­weise durch Drücken des Enter- oder Cancel- But­tons durch den Be­nutzer be­endet. 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 daher nicht ga­ran­tiert, dass die Haupt­schlei­fe inner­halb ei­ner be­stimm­ten Zeit durch­läuft. Tatsächlich läuft die Hauptschleife im Idealfall über­haupt nicht durch sondern stoppt mit dem Aufruf des Haupt­menüs, das nie­mals be­endet wer­den sollte.

Flags wie z.B. das Sekunden­flag oder Kom­muni­kations­flags wer­den inner­halb des Screens abge­handelt.

Ein Screen enthält Widgets wie Texte, Buttons oder Eingabe­felder.

Diese sind in einer C-Struct zusammen­gefasst. Jedes dieser Widgets wird nach­ein­ander auf das Display geschrie­ben, was normaler­weise nur ein ein­ziges Mal nötig wäre.

Ein Auto­refresh-Widget wurde einge­führt um beispiels­weise Mess­werte anzu­zeigen, die sich auch ohne User-Inter­ven­tion ändern kön­nen. Die­se wer­den etwa drei­mal pro Se­kunde aktu­alisiert. Die­se Fre­quenz kann der Be­nutzer op­tisch noch erfas­sen und ermög­licht doch ra­sche R­eak­tio­nen auf un­er­wartete Än­de­run­gen. We­sent­lich höhere Fre­quen­zen wür­den nur als stö­ren­des Flim­mern ohne zu­sätz­li­chen In­for­ma­tions­ge­halt wahr­ge­nom­men, we­sent­lich niedrigere würden die Reaktions­fähigkeit ver­ringern und den Be­nutzer un­ge­dul­dig wer­den lassen.

2..3 Hz sind dafür ein guter Wert. Falls Ihr Ge­rät unter nied­rigen Um­gebungs­tem­peraturen arbeiten muss evtl. auch weniger, da das Display dann träger wird und rasche Änderungen nicht lesbar ab­bilden kann.

Screen Manual Test

Dieser Screen dient zum manuel­len Test der Quel­le unter kon­stanter Last. Der Strom kann über die Tastatur ein­gegeben werden und/oder durch Dre­hen des En­coders.

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

Set Point ist dabei der ein­ge­stellte Soll­wert. In der Zei­le da­run­ter wird der tat­säch­li­che Strom und die anlie­gende Span­nung an­ge­zeigt. Da­run­ter se­hen Sie die da­raus be­rech­nete Ver­lust­leistung der Sen­ke (nicht not­wen­diger­weise der Quelle wenn da noch ohmsche An­teile vor­han­den sind!) sowie umge­rech­net, den Wi­der­stand den die Quel­le sieht. Darin ent­halten ist auch der Leis­tungs­verlust in der Si­che­rung da der Span­nungs­abfall über die­ser mit gemes­sen wird. Un­berück­sichtigt bleibt, da unbe­kannt, die Leis­tung über der rest­li­chen Ver­ka­be­lung, so dass die Strom­quelle tat­säch­lich et­was mehr Leis­tung ein­speist. In der drit­ten Zei­le se­hen wir die Tem­peratur des Kühl­kör­pers (THS=Temp Heat Sink) sowie 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-Button 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, jeder­zeit durch Drücken von 0..9 ei­ner der zehn Pre­sets aufge­ru­fen wer­den oder, falls wir uns im Set Point Wid­get befinden, der Strom durch Dre­hen des Inkre­mental-Ge­bers in Schrit­ten der ak­tuel­len Cur­sor­po­si­ti­on 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 abzu­schal­ten falls die Situ­ation dies er­for­dert.

Ein lan­ger Druck einer Zif­fern­taste (außer 0) spei­chert den ak­tu­el­len Strom als den ent­spre­chen­den Pre­set.

Dieser Screen eig­net sich sehr gut um die Strom­quel­le an ihren Gren­zen oder mittels 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 direkt in mA ein­ge­stellt werden. Alter­nativ 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­tuell ein­ge­stell­ten Wert ge­setzt wer­den.

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

Status-Anzeige

Die Status-An­zei­ge sieht so aus:

1.234A 12.345V
12.34W 12.34Ω
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. Zusätz­lich sehen wir die Tem­pe­ra­tu­ren am Kühl­körper sowie im Ge­häu­se und au­ßer­dem die Span­nungen P12 und VCC (P5)

Der Stat-Screen wird durch Drücken einer belie­bigen Taste be­endet und führt zurück zum Menü.

Eingabefelder

Eingabefelder er­lau­ben die Ein­ga­be von nu­me­ri­schen Wer­ten über die Tas­tatur und/oder den En­co­der. Ein Ein­ga­be­feld wird durch die Enter-Tas­te be­tre­ten und kann dann über die Zif­fern­tas­tatur oder den En­co­der ver­än­dert wer­den. Das bringt uns in eine Zwick­müh­le. Ei­ner­seits sol­len Ver­tip­per keine un­gewoll­te Än­de­rung des Stromes be­wir­ken, an­de­rer­seits soll mit dem En­co­der in Echt­zeit eine Fein­jus­tage des Stromes mög­lich sein. Was soll also pas­sie­ren, wenn der User ein paar Stel­len tippt und dann am En­co­der dreht? Die ak­tu­elle 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­ßende Strom wird jedoch noch nicht geändert. Beim Dre­hen des En­co­ders wird der Wert in der ak­tuel­len Cur­sor­po­si­ti­on 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­gabe ge­ändert 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 einge­stellt.
Beim Ver­lassen des Fel­des nach rechts (durch Drücken von Enter) wird der ein­ge­stell­te Wert als Strom ge­setzt.

Das kann durchaus bewusst aus­ge­nutzt wer­den um Last­sprün­ge ma­nu­ell zu er­zeu­gen: Stellen Sie den Strom für das Ende des Last­sprungs zu­nächst ein und ver­las­sen Sie das Feld mit Enter. Dann be­treten Sie das Feld erneut und stel­len Sie den Wert für den Beginn des Last­sprungs mit dem Encoder ein. Wenn Sie nun das Feld mit Cancel verlassen wird der Strom schlag­artig auf den ur­sprüng­lichen Wert zu­rück­ge­stellt und Sie ha­ben ihren Last­sprung!

USB

Die USB-Schnittstelle ist na­tür­lich gal­va­nisch vom Rest der Schal­tung iso­liert um Rück­wir­kungen und mö­glicher­weise fa­tale Schlei­fen­ströme über den steu­ern­den PC (dessen Mas­se ja im Re­gel­fall ge­erdet ist) zu ver­hin­dern.

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

Beachten Sie, dass auch Tran­sien­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­ander ver­bun­den.

Senden über den USB erfolgt klas­sisch über einen Sen­de­puffer im Ge­rät. Bei ei­nge­henden Da­ten wer­den die­se gesam­melt bis ein LF ein­trifft. Dann wird die gesamte Zeile als Be­fehl ausge­wertet. Der Host soll­te kei­ne wei­te­ren Daten sen­den bis er die Ant­wort auf diesen er­hält da sonst Zei­chen ver­lo­ren gehen 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 lasse 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 in­con­venience...

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 con­siderably 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 trans­ferred 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 pre­ferrable, 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.

ot: 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.