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 vorgesehen doch es erwies sich als zu langsam für den Enconder. Dieser hat seine Schaltflanken nämlich, aus mir unverständlichen Gründen, nicht zwischen den Steps, sondern währenddessen und erzeugt damit während eines 'Clicks' zwei Flanken. Mit 10 ms zwischen den Ab­tas­tungen ist das nicht zuver­lässig machbar.

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-Interrupt gescannt. 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 jede En­coder-Aktion wird in einen Puffer ge­schrie­ben und kann vom Haupt­programm mit­tels getKey() abge­fragt werden, was diesen Puffer 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 Encoder-Buttons wird als KEY_ENTER über­mittelt könnte aber natür­lich bei Be­darf auch einen 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 Menu wurde imple­men­tiert. Um auch mehr als drei Menüeinträge zu erlauben (die Titelleiste und das eigent­liche Menü) wurde es scroll­fähig gestaltet. Ein Menü kann damit bis zu 127 Einträge umfassen. Das wäre natürlich als UI untauglich aber mehr als drei sind schon ab und zu nötig... Anwählen eines Menüpunktes ruft eine ent­spre­chen­de Call­back-Funk­tion auf welche die ent­spre­chen­de Funk­tionali­tät re­alisiert. Wenn die Callback-Funktion NULL ist wird das Menü beendet und kehrt zur auf­rufen­den Funktion zurück. Das Haupt­menü sollte das nicht tun, wenn­gleich die Haupt­schleife in diesem Falle das Haupt­menü erneut starten würde aber das wäre nur be­lastend für den User.
Der Aufruf eines Menus hält das aufrufende Programm bis zu seinem Ende an. Flags (wie Timer- oder Kom­munikations­flags) werden fortan inner­halb der Menu-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ührung bis zu seinem Ende an­hält. Er/es ist in dieser Zeit eben­falls für die Bear­beitung der Flags verant­wort­lich.

Die Routine 'doFlags' wird dazu nach jedem Sleep aufgerufen und erledigt alles nötige.

Sonder­aktionen wie z.B. die Kali­brierung, der Fer­ti­gungs­test oder die Program­mierung von Zeitkon­stanten können nur über den USB-Anschluss durchge­führt werden. Natürlich könnte auch ein ent­sprechen­des Menü ent­worfen werden aber wegen der Sel­ten­heit dieser Ereig­nisse wäre das nur Ver­schwen­dung von Resourcen.

PROGMEM

Soll dass Menü samt seiner Kom­po­nen­ten (der Menü­einträ­ge) im Flash oder im RAM gehalten werden?
Es gibt Ar­gumen­te für be­ides. Ein Menü im RAM kann vom Haupt­programm jederzeit an­ge­passt, um zusätz­liche Ein­träge ergänzt oder um nicht sinn­volle Op­tionen ge­kürzt werden. Allerdings ist der Speicher­verbrauch beträcht­lich, wenn unser System nur zwei Kilo­bytes RAM besitzt.

Ich habe das Menü so program­miert, dass es kom­plett im Flash-Speicher ge­hal­ten wird. Dyna­misch er­scheinende oder ver­schwin­dende Ein­träge halte ich ohne­hin für eher irri­tierend (in der Hilfe steht, drücken sie .. um etwas zu tun, aber warum .. nicht erscheint oder ausge­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­gerufen wird, hält er die Haupt­schleife bis zu seinem Ende an. Die Haupt­schleife redu­ziert sich des­halb auf eine Reihe von Screens oder Sub­menues, die nach­einander aufge­rufen werden, 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"- Buttons durch den Be­nutzer beendet. Es kann aber auch ein Time­out vorge­geben wer­den, wie es z.B. beim Begrüs­sungs­bildschirm sinnvoll ist.

Es ist daher nicht garantiert, dass die Haupt­schleife inner­halb einer 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 werden 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­vention ändern können. Diese werden etwa dreimal pro Se­kunde aktu­alisiert. Diese Frequenz kann der Benutzer optisch noch erfas­sen und ermög­licht doch rasche Reaktionen auf uner­wartete Än­derungen. Wesent­lich höhere Fre­quen­zen würden nur als störendes "Flimmern" ohne zusätzlichen Informations­gehalt wahr­genom­men, wesent­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 einge­stellte Soll­wert. In der Zeile darunter wird der tat­sächliche Strom und die anlie­gende Span­nung ange­zeigt. Darunter sehen Sie die daraus be­rech­nete Verlust­leistung der Senke (nicht not­wen­diger­weise der Quelle wenn da noch ohmsche An­teile vor­han­den sind!) sowie umge­rechnet, den Wider­stand den die Quelle sieht. Darin ent­halten ist auch der Leistungs­verlust in der Sicherung da der Span­nungs­abfall über dieser mit gemes­sen wird. Unberück­sichtigt bleibt, da unbe­kannt, die Leis­tung über der restlichen Ver­kabelung, so dass die Strom­quelle tat­säch­lich etwas mehr Leistung ein­speist. In der dritten Zeile sehen wir die Tem­peratur des Kühl­körpers (THS=Temp Heat Sink) sowie die Tem­peratur im Gehäuse (TIC=Temp Inside Case). Das "<"-Zeichen ist der (abgekürzte) Back-Button mit dem dieser Screen be­endet wer­den kann.

In diesem Screen kann, sofern nicht gerade das 'Set Point'-Widget be­ar­beitet wird, jeder­zeit durch Drücken von 0..9 einer der zehn Pre­sets aufge­rufen werden oder, falls wir uns im 'Set Point' Widget befinden, der Strom durch Drehen des Inkre­mental-Gebers in Schritten der ak­tuel­len Cursor­po­sition erhöht oder ver­ringert werden.

Der Preset "0" ist auf 0 mA gesetzt und kann nicht verändert werden. So ist es stets möglich, den Strom durch drücken der '0'-Taste schnell abzu­schal­ten falls die Situ­ation dies erfordert.

Ein langer Druck einer Ziffern­taste (ausser 0) speichert den altuellen Strom als den ent­spre­chenden Preset.

Dieser Screen eignet sich sehr gut um die Strom­quelle an ihren Gren­zen oder mittels der Presets gegen Last­sprünge zu testen.

Einstellen der Presets

Die zehn Presets können über die USB-Schnitt­stelle per Kommando direkt in mA einge­stellt werden. Alter­nativ können Sie auch in der Standard-Anzeige durch langes Drücken einer Zif­fern­taste auf den ak­tuell einge­stellten Wert gesetzt werden.

Sie werden dann direkt im EEPROM ver­ewigt und stehen auch nach einem Neu­start des Geräts zur Ver­fügung.

Status-Anzeige

Die Status-Anzeige 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 sehen wir die an­lie­gende Span­nung, den flies­senden Strom, die Verlust­leistung in der Senke sowie den äqui­va­len­ten Wider­stand, den die Sen­ke für die Strom­quelle dar­stellt. Zusätz­lich sehen wir die Tem­pe­ra­tu­ren am Kühl­körper sowie im Ge­häuse und aus­ser­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 erlauben die Eingabe von numerischen Werten über die Tastatur und/oder den Encoder. Ein Eingabefeld wird durch die Enter-Taste betreten und kann dann über die Ziffern­tas­tatur oder den Encoder ver­ändert werden. Das bringt uns in eine Zwick­mühle. Einerseits sollen Ver­tip­per keine ungewollte Änderung des Stromes bewirken, anderer­seits soll mit dem Encoder in Echt­zeit eine Fein­jus­tage des Stromes möglich sein. Was soll also pas­sieren, wenn der User ein paar Stel­len tippt und dann am Encoder dreht? Die ak­tu­elle Soft­ware macht fol­gendes:
Wenn Zifferntasten gedrückt werden, wird der Wert des Feldes ent­sprechend geändert, der tat­säch­lich flies­sende Strom wird jedoch noch nicht geändert. Beim Drehen des Encoders wird der Wert in der ak­tuel­len Cursor­position um eins erhöht/ernied­rigt und der Strom ent­sprechend ein­ge­stellt, selbst wenn der Wert vorher durch Zif­fern-Ein­gabe geändert wurde.
Beim Verlassen des Feldes nach links (durch Drücken von Cancel) wird der Wert vor dem Be­treten wieder her­ge­stellt und der Strom ent­sprechend einge­stellt.
Beim Ver­lassen des Feldes nach rechts (durch Drücken von Enter) wird der ein­gestellte Wert als Strom ge­setzt.

Das kann durchaus bewusst ausgenutzt werden um Last­sprünge manuell zu erzeugen: Stellen Sie den Strom für das Ende des Last­sprungs zunächst ein und verlassen 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 zurück­gestellt und Sie haben ihren Last­sprung!

USB

Die USB-Schnittstelle ist na­tür­lich gal­vanisch 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 Masse ja im Regel­fall ge­erdet ist) zu ver­hin­dern.

Die Trennung ist bis zu einem Wert von 500 V= zwi­schen der Eload-Masse und USB-GND ga­ran­tiert.

Beachten Sie, dass auch Tran­sienten diesen Wert nicht über­schreiten dürfen! USB-Masse und Schaltungs-Masse sind mit 450 kΩ || 2,2 nF mit­ein­ander ver­bunden.

Senden über den USB erfolgt klas­sisch über einen Sende­puffer im Ge­rät. Bei einge­henden Daten werden diese gesammelt bis ein LF eintrifft. Dann wird die gesamte Zeile als Befehl ausge­wertet. Der Host sollte keine wei­teren Daten sen­den bis er die Ant­wort auf diesen erhält da sonst Zei­chen ver­loren gehen kön­nen.

Entschuldigung :-)

Für den Rest dieser Seite bin ich leider aus nicht mehr nachvollziehbaren Gründen ins Englische gerutscht. Ich lasse es aber so bis das Eload-Kapitel einen "vorläufig entgültigen" Zustand erreicht 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 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.