Prozeda-Decoder: Unterschied zwischen den Versionen

Aus Hobbyelektronik.org
K
Zeile 6: Zeile 6:
 
Die anfängliche Vermutung, dass der Regler von Resol kommt verflog relativ schnell (Es gab wohl einen Wechsel der Plattform), tatsächlich handelt es sich um ein Gerät von Prozeda.
 
Die anfängliche Vermutung, dass der Regler von Resol kommt verflog relativ schnell (Es gab wohl einen Wechsel der Plattform), tatsächlich handelt es sich um ein Gerät von Prozeda.
  
Ein paar Mails später gab es einen Link auf ein [http://www.forum-raspberrypi.de/Thread-projekt-solar-data-logger Raspberry Pi-Forum] - bis auf einen toten Link waren die Informationen relativ karg, außer dass in dem Datastick, der zum Loggen der Betriebsdaten verwendet wird, ein AT45DB081D steckt und am PC ein FTDI2232 im GPIO-Mode verwendet wird und wie die Pinbelegung am Datastick (oder der Buchse am Regler?) ist. Leider ist die Diskussion versandet und es gab keine weiteren Informationen.
+
Ein paar Mails später gab es einen Link auf ein [http://www.forum-raspberrypi.de/Thread-projekt-solar-data-logger Raspberry Pi-Forum] - bis auf einen toten Link waren die Informationen relativ karg, außer dass in dem Datastick, der zum Loggen der Betriebsdaten verwendet wird, ein AT45DB081D steckt und am PC ein FT2232 im GPIO-Mode verwendet wird und wie die Pinbelegung am Datastick (oder der Buchse am Regler?) ist. Leider ist die Diskussion versandet und es gab keine weiteren Informationen.
  
 
Aber ich war am Haken ;-)
 
Aber ich war am Haken ;-)

Version vom 19. April 2018, 20:57 Uhr

RATIOfresh 200 von Wagner Solar

"Funktioniert das auch mit meiner Solaranlage?" ist eine immer wiederkehrende Frage, nachdem ich den Artikel zum VBus-Decoder veröffentlicht habe. "Vielleicht, aber wahrscheinlich eher nein", war die häufigste Antwort.

So in etwa auch bei der E-Mail von Frank und seiner RATIOfresh 200 von Wagner & Co Solartechnik, die er an den PC bzw. einen Single Board Computer (wie den Raspberry Pi) anbinden möchte um sich das ewige Gerenne in den Keller und das Auslesen des Datenloggers zu sparen.

Die anfängliche Vermutung, dass der Regler von Resol kommt verflog relativ schnell (Es gab wohl einen Wechsel der Plattform), tatsächlich handelt es sich um ein Gerät von Prozeda.

Ein paar Mails später gab es einen Link auf ein Raspberry Pi-Forum - bis auf einen toten Link waren die Informationen relativ karg, außer dass in dem Datastick, der zum Loggen der Betriebsdaten verwendet wird, ein AT45DB081D steckt und am PC ein FT2232 im GPIO-Mode verwendet wird und wie die Pinbelegung am Datastick (oder der Buchse am Regler?) ist. Leider ist die Diskussion versandet und es gab keine weiteren Informationen.

Aber ich war am Haken ;-)

Einblick in den Regler

Ein Griff in die Zukunft - 11/2017:

Im Rahmen der Pirozeda-Entwicklung hat mir Hans freundlicherweise seinen zweiten Prozeda-Regler zur Verfügung gestellt. Natürlich habe ich einen vorsichtigen Blick hinein geworfen:

Controllerboard

Soweit so unspektakulär. Leider ist auf dem Controllerboard nicht zu sehen, welcher Mikrocontroller verwendet wird - dieser versteckt sich hinter dem Display, das ich aus verständlichen Gründen nicht auslöten wollte.

Interessant ist, dass sich links oben ein kleiner nicht so richtig schöner Hotfix befindet: Zwischen den Beinchen von V25 wurde relativ knapp ein 0603-Widerstand eingelötet. Auf der Rückseite befindet sich ebenfalls ein Bauteil, das so wohl nicht geplant war.

Abgesehen davon macht das Board einen relativ aufgeräumten Eindruck, auch wenn mir die unten die Massefläche nicht so gut gefällt und sie oben gänzlich fehlt.

Auffällig ist der nicht weiter beschriebene Footprint über S3 (im unteren Bereich der Leiterkarte): Das sieht sehr nach einem Platzhalter für einen Mini-DIN-Stecker aus. Leider habe ich hier keine Messungen durchgeführt...

Leistungsboard

So richtig gut gefällt mir das LEistungsboard ehrlich gesagt nicht. Die Isolationsabstände sind mir etwas knapp und die Schaltmodule selbst machen zwar einen guten Eindruck, aber die Integration ist nicht so richtig schön bzw. hätte ich Angst, dass sie bei einem Fall herunterbrechen.

Die Schaltmodule bestehen aus einem MOC3063 und einem ST T1235-600G (12 A Triac) sowie ein bisschen Hühnerfutter.

Daneben gibt es noch einen potenzialfreien Kontakt mit Relais von Finder.

Die Buchse für den Datastick ist keine große Überraschung, bis auf dass ihr Schirm nicht verbunden ist. Für den Datastick sicher irrelevant, für das optionale Gateway wäre eine richtig geschirmte Leitung zumindest kein Nachteil. Neben den 3,3 V liegt an der Buchse auch direkt die Ausgangsspannung des Netzteil. Mir ist aufgrund des dämlich platzierten Aufklebers auf dem Trafos zwar kein offizielles Rating bekannt, aber mit der Versorgung sollte man zumindest ein bisschen Spaß haben können.

Der Datenstick

Wie bereits erwähnt verbirgt sich im Stick nichts mehr als ein Atmel AT45DB081D - ein 8 MBit großer Flash-Speicher mit SPI als Interface.

Dieser wird über einen 8-poligen Mini-DIN-Stecker mit dem Regler bzw. dem Adapter zum Auslesen am PC verbunden.

Pin Bezeichnung
1 17 V
2 unbekannt
3 MISO
4 GND
5 3,3 V
6 MOSI
7 SCK
8 !CS

Datenformat

Mit der PC-Software lassen sich die Daten vom Logger sowohl als Rohdaten als auch im CSV-Format auf dem PC speichern. Beide Datensätze habe ich bekommen:

aa5540052401ffffffffffffffffff0020205761676e6572202620436f2000000000000000000000000020536f6c617220536f6c6172746563686e696b20...
;Datalogging
;  Wagner & Co 
;
; Solartechnik 
XXXXXXXXXXXXXXXX
;  System-Nr.  	XXXX
; System-Ver.  	XXX
Datum	Uhrzeit	Kollektor	Speicher  unten	Speicher  oben	Rücklaufanh.	Rücklaufanh.	T	T	Primär Vorlauf	Kaltwasser	T	Frischwasser	Ausgang 1	Ausgang 2	Ausgang 3	Ausgang 4	Ausgang 5	Ausgang 6	Ausgang 7	unused	Speicher	Speicher	Funktion aktiv	Funktion aktiv	Funktion aktiv	Funktion aktiv	Durchfluss	Zapfung
8	9	1	1	1	1	1	1	1	1	1	1	1	10	10	10	10	10	10	10	10	7	15	11	11	11	11	19	27

09.11.16	18:44:00	-2.100	45.800	48.000	225.100	225.100	225.100	225.100	63.200	46.800	150.700	54.300	0.000	0.000	0.000	0.000	100.000	8.000	0.000	0.000	0	2249	0.100	0.200	0.100	0.200	0.000	-0.400
09.11.16	18:44:00	-2.100	45.700	48.000	228.300	228.300	228.300	228.300	63.300	46.200	200.400	54.400	0.000	0.000	0.000	0.000	100.000	9.000	0.000	0.000	0	2249	0.100	0.200	0.100	0.200	0.000	-0.200
09.11.16	18:44:00	-2.000	45.600	47.900	228.300	228.300	228.300	228.300	63.300	45.800	200.400	54.300	0.000	0.000	0.000	0.000	100.000	7.000	0.000	0.000	0	2249	0.100	0.200	0.100	0.200	0.000	-0.500
09.11.16	18:44:00	-2.000	45.600	47.900	231.000	231.000	231.000	231.000	63.300	45.700	225.200	54.300	0.000	0.000	0.000	0.000	100.000	6.000	0.000	0.000	0	2249	0.100	0.200	0.100	0.200	0.000	-0.600
09.11.16	18:44:00	-2.100	45.500	47.900	231.000	231.000	231.000	231.000	63.300	45.500	225.200	54.500	0.000	0.000	0.000	0.000	100.000	7.000	0.000	0.000	0	2249	0.100	0.200	0.100	0.200	0.000	-0.500
09.11.16	18:44:00	-2.000	45.500	47.800	233.300	233.300	233.300	233.300	63.300	45.500	237.600	54.500	0.000	0.000	0.000	0.000	100.000	8.000	0.000	0.000	0	2249	0.100	0.200	0.100	0.200	0.000	-0.400

Die Systemkennziffern (Seriennummer, System-Nr. und -Version) habe ich durch "X" ersetzt.

Die Binärdatei gehört eindeutig zur CSV, nur wie ist der Zusammenhang? Einen Schritt zurück - erstere ist genau 2048 KiB groß, der verwendete Flash-Speicher aber nur 1 MiB. Beim Blick in die Datei: das sieht alles ziemlich hexadezimal aus.

Um schnell zum Ziel zu kommen und die Codeeffizient erst einmal egal ist, habe ich PHP verwendet um die Daten umzuwandeln:

function convert($infile, $outfile)
{
    $if = fopen($infile, "rb");
    $of = fopen($outfile, "wb");

    while(!feof($if))
    {
        $str = fread($if, 2);
        fwrite($of, hex2bin($str));
    }

    fclose($if);
    fclose($of);
}

Nach ein paar Sekunden konnte ich das Ergebnis in HxD bewundern:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000  AA 55 40 05 24 01 FF FF FF FF FF FF FF FF FF 00  ªU@.$.ÿÿÿÿÿÿÿÿÿ.
00000010  20 20 57 61 67 6E 65 72 20 26 20 43 6F 20 00 00    Wagner & Co ..
00000020  00 00 00 00 00 00 00 00 00 00 20 53 6F 6C 61 72  .......... Solar
00000030  20 53 6F 6C 61 72 74 65 63 68 6E 69 6B 20 XX XX   Solartechnik XX
00000040  XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX  XXXXXXXXXXXXXXXX
00000050  20 20 53 79 73 74 65 6D 2D 4E 72 2E 20 20 XX XX    System-Nr.  XX
00000060  20 53 79 73 74 65 6D 2D 56 65 72 2E 20 20 XX XX   System-Ver.  XX
...

mehr Daten

Das sieht doch gut aus!

Speichersegmente

Die nun sichtbaren Daten erzeugen den Eindruck von 3 Datenbereichen:

Start-Adresse Ende-Adresse Länge Inhalt
0x00000 0x0006F 0x0006F Systeminfo
0x00200 0x0043F 0x00240 Spaltenbeschreibung
0x00500 0xFFFFF 0xFFB00 Loggingdaten

Systeminfo

Die ersten beiden Byte auf dem Flash lauten 0xAA 0x55, die schon beim Initialisieren des Sticks geschrieben werden. Gleiches gilt für das dritte Byte (Adresse 0x00002), das vermutlich das Logging-Intervall repräsentiert. Leider konnte ich das bis jetzt noch nicht so richtig nachvollziehen.

Ab Offset 0x00010 befindet sich der Herstellername der Solaranlage. Am Anschluss - ab Offset 0x0003E steht die 18-stellige Seriennummer und ab 0x00050 die Systemnummer, wobei der eigentliche Wert bei 0x0005E mit Länge 2 liegt. Gleiches gilt für die Systemversion, die ab Offset 0x00060 beschrieben und der eigentliche Wert bei 0x0006E - ebenfalls mit Länge 2 liegt.

Die beiden letzteren Werte für letztere Bezeichner sind binär Little-Endian abgespeichert, aus 0x12 0x34 würde also 0x3412 bzw. 13330 werden.

Spaltenbeschreibung

Die Spaltenbeschreibungen sind wie folgt aufgebaut: 16 Zeichen lang, 15 davon sind NULL-terminierte ASCII-Strings gefolgt von einem weiteren Zeichen:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
...
00000200  20 20 20 20 44 61 74 75 6D 20 20 20 20 20 00 08      Datum     ..
00000210  20 20 20 55 68 72 7A 65 69 74 20 20 20 20 00 09     Uhrzeit    ..
00000220  20 53 65 6B 75 6E 64 65 6E 20 00 00 00 00 00 10   Sekunden ......
00000230  20 20 4B 6F 6C 6C 65 6B 74 6F 72 20 20 20 00 01    Kollektor   ..
00000240  20 20 53 70 65 69 63 68 65 72 20 B9 20 20 00 01    Speicher ¹  ..
...

Eine Besonderheit stellen offenbar die ASCII-Zeichen 0xB9 und 0xB3 dar - diese werden beim Auslesen nach "unten" und "oben" übersetzt.

Dieses letzte Zeichen spiegelt sich in der Zeile nach den Spaltenköpfen in der CSV-Datei wider. Nach genauerer Betrachtung ergibt sich folgende (mutmaßliche) Zuordnung:

Wert (Hex) Datentyp Länge
00 Dummy 2?
01 Temperatur 2
07 Speicher 4?
08 Datum 2
09 Uhrzeit 2
0A Ausgang 1
0D Fehler 1?
0E Fehler 2?
0F Speicher 2?
10 Sekunden 2
13 Durchfluss 2
1B Zapfung 2

Zur Länge später mehr.

Loggingdaten

Ab Adresse 0x00500 wird es interessant, hier befindet sich die Payload. Als "Trick" um die Datenlänge besser ermitteln zu können hat sich bewährt, die Anzeigebreite des Hex-Editors auf Fenstergröße anpassen zu lassen und dann die Breite des Fensters zu variieren. Dadurch lassen sich Wiederholungen besser erkennen. So bei 64 Byte Breite:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 
...
00000500  55 04 64 04 2F 00 EB FF CA 01 E0 01 CB 08 CB 08 CB 08 CB 08 78 02 D4 01 E3 05 1F 02 00 00 00 00 64 08 00 00 00 55 55 00 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FC FF 00 00  U.d./.ëÿÊ.à.Ë.Ë.Ë.Ë.x.Ô.ã.......d....UU.......É.............üÿ..
00000540  55 04 64 04 30 00 EB FF C9 01 E0 01 EB 08 EB 08 EB 08 EB 08 79 02 CE 01 D4 07 20 02 00 00 00 00 64 09 00 00 00 55 55 00 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FE FF 00 00  U.d.0.ëÿÉ.à.ë.ë.ë.ë.y.Î.Ô. .....d....UU.......É.............þÿ..
00000580  55 04 64 04 31 00 EC FF C8 01 DF 01 EB 08 EB 08 EB 08 EB 08 79 02 CA 01 D4 07 1F 02 00 00 00 00 64 07 00 00 00 55 55 00 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FB FF 00 00  U.d.1.ìÿÈ.ß.ë.ë.ë.ë.y.Ê.Ô.......d....UU.......É.............ûÿ..
000005C0  55 04 64 04 32 00 EC FF C8 01 DF 01 06 09 06 09 06 09 06 09 79 02 C9 01 CC 08 1F 02 00 00 00 00 64 06 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FA FF 00 00  U.d.2.ìÿÈ.ß.........y.É.Ì.......d....UU.......É.............úÿ..
00000600  55 04 64 04 33 00 EB FF C7 01 DF 01 06 09 06 09 06 09 06 09 79 02 C7 01 CC 08 21 02 00 00 00 00 64 07 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FB FF 00 00  U.d.3.ëÿÇ.ß.........y.Ç.Ì.!.....d....UU.......É.............ûÿ..
00000640  55 04 64 04 34 00 EC FF C7 01 DE 01 1D 09 1D 09 1D 09 1D 09 79 02 C7 01 48 09 21 02 00 00 00 00 64 08 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FC FF 00 00  U.d.4.ìÿÇ.Þ.........y.Ç.H.!.....d....UU.......É.............üÿ..
00000680  55 04 64 04 35 00 EC FF C6 01 DE 01 1D 09 1D 09 1D 09 1D 09 79 02 C8 01 48 09 20 02 00 00 00 00 64 07 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FB FF 00 00  U.d.5.ìÿÆ.Þ.........y.È.H. .....d....UU.......É.............ûÿ..
000006C0  55 04 64 04 36 00 EC FF C6 01 DE 01 32 09 32 09 32 09 32 09 79 02 C8 01 86 09 20 02 00 00 00 00 64 07 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FB FF 00 00  U.d.6.ìÿÆ.Þ.2.2.2.2.y.È.?. .....d....UU.......É.............ûÿ..
00000700  55 04 64 04 37 00 EB FF C6 01 DE 01 32 09 32 09 32 09 32 09 79 02 C7 01 A5 09 21 02 00 00 00 00 64 06 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FA FF 00 00  U.d.7.ëÿÆ.Þ.2.2.2.2.y.Ç.¥.!.....d....UU.......É.............úÿ..
00000740  55 04 64 04 38 00 EB FF C5 01 DE 01 45 09 45 09 45 09 45 09 79 02 C7 01 B4 09 21 02 00 00 00 00 64 07 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FB FF 00 00  U.d.8.ëÿÅ.Þ.E.E.E.E.y.Ç.´.!.....d....UU.......É.............ûÿ..
00000780  55 04 64 04 39 00 EB FF C5 01 DD 01 45 09 45 09 45 09 45 09 79 02 C6 01 BC 09 22 02 00 00 00 00 64 08 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FC FF 00 00  U.d.9.ëÿÅ.Ý.E.E.E.E.y.Æ.¼.".....d....UU.......É.............üÿ..
000007C0  55 04 64 04 3A 00 EB FF C5 01 DD 01 54 09 54 09 54 09 54 09 78 02 C6 01 C0 09 23 02 00 00 00 00 64 07 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FB FF 00 00  U.d.:.ëÿÅ.Ý.T.T.T.T.x.Æ.À.#.....d....UU.......É.............ûÿ..
00000800  55 04 65 04 00 00 EB FF C5 01 DD 01 54 09 54 09 54 09 54 09 7A 02 C7 01 C2 09 23 02 00 00 00 00 64 06 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FA FF 00 00  U.e...ëÿÅ.Ý.T.T.T.T.z.Ç.Â.#.....d....UU.......É.............úÿ..
00000840  55 04 65 04 01 00 EB FF C4 01 DD 01 62 09 62 09 62 09 62 09 79 02 C7 01 C3 09 21 02 00 00 00 00 64 07 00 00 00 55 55 10 00 00 00 00 00 00 C9 08 00 00 01 00 02 00 01 00 02 00 00 00 FB FF 00 00  U.e...ëÿÄ.Ý.b.b.b.b.y.Ç.Ã.!.....d....UU.......É.............ûÿ..
...

Zieht man die Spaltenbeschreibungen zu Rate, müssen die ersten Bytes das Datum und die Uhrzeit sein. Und tatsächlich, bei Offset 0x04 sieht man, dass der Wert hochzählt. Angekommen bei 0x3A nullt der Wert und an Offset 0x02 wird hochgezählt. 0x3A ist allerdings nicht 59, sondern 58. Vielleicht eine leichte Asynchronität zwischen Uhr und Aufzeichnung?

Scrollt man weiter nach unten, ändern sich auch die Bytes davor. Interessant ist hierbei, dass sich die geraden Bytes öfter ändern als die ungeraden. Oder: die "ersteren" scheinen das kleine Ende der entsprechenden Zahl zu repräsentieren. Unser System scheint Little Endian zu verwenden. Interessant ist auch, dass bei Offset 0x05 konsequent eine Null steht. Wenn es sich tatsächlich um Sekunden handelt, warum haben die Entwickler hier sinnlos Platz verschwendet?

Ungeduldig wie ich bin (das da vorne _muss_ ein Timestamp sein), habe ich mir die nächsten Bytes angesehen. Offset 0x06 eiert um 0xEB herum, Offset 0x07 ist für länger konstant 0xFF, "schnappt" aber irgendwann auf 0x00 um. Da es unwahrscheinlich ist, dass sich ein Wert sehr stark ändert, tippe ich 0xFFEB in den Taschenrechner ein, stelle ihn auf Word-Breite ein und bekomme direkt -21 ausgespuckt (der Windows-Taschenrechner verwendet signed Datentypen). Ein Blick nach oben in die Daten der CSV-Datei verrät -2,1 °C. Die nächsten zwei Byte (bei Offset 0x08 und 0x09) lauten 0xCA 0x01, der Taschenrechner macht aus 0x01CA die dezimale Entsprechung 458. Die CSV-Tabelle sagt in der Spalte der ersten Zeile 45,8 °C. Das war einfach!

Um die Daten besser untersuchen zu können, habe ich das Swiss-Army-Knife aller BWLer und das Hacking-Tool schlechthin verwendet: Excel.

Mit Den Spaltenköpfen und der ersten Logzeile aus der CSV-Datei und der ersten Zeile (Hex-Codiert) aus der Binärdatei mache ich mich ans Werk.

Spalte F dient dazu den Offset für einen Wert vorzugeben, Spalte G bestimmt die Länge der in Spalte H separierten Hex-Werte. In Spalte I werden die ersten beiden Teile getauscht (ok, das funktioniert nur für maximal 2 Byte lange Werte) und von Hexadezimal nach Dezimal umgerechnet.

In Spalte J wird von Unsigned nach Signed umgewandelt (was allerdings nur für 2 Byte lange Datensätze funktioniert). Nach etwas Schätzen, Daten vorwärts und rückwärts durchgehen, ergab sich die Tabelle für die Spaltenbeschreibung von oben. Bei der Länge der Datenfelder für Fehler bin ich mir nicht 100 % sicher aber es sieht sehr plausibel aus.

Zurück zum Datum und der Uhrzeit. Da steht bei dem Satz 1109 und 1124, was dem 09.11.2016 und 18:44 entsprechen soll. Jetzt kann man lang überlegen, mit dem Tag seit Datum X rechnen oder einfach einen Strich zwischen 11 und 09 packen. Eine Jahresangabe sucht man vergebens. Bei der Uhrzeit ist es naheliegend, die Sekunden oder Minuten seit Mitternacht zu zählen - also einfach mal geteilt durch 60, ergibt: 18,73 - 18 Uhr passt, 0,73 entspricht den 44 Minuten. Haken dran. Die Sekunden werden von der PC-Software interessanterweise nicht ausgegeben, durch die Spaltenbeschreibung im Flash muss die dritte Spalte aber den Sekunden-Anteil repräsentieren.

Ein Datensatz aufgedröselt sieht dann wie folgt aus:

Kommunikation mit dem Datastick

Wie die Daten im Speicher aussehen ist nun bekannt, aber wie kommen sie drauf?

Frank war so freundlich und hat mir Traces geschickt. Leider zunächst nur Exports, in denen ich das genaue Timing und "alles andere" nicht sehen konnte.

Wie André Lampe in seinem 33C3-Vortrag "Es sind die kleinen Dinge im Leben" sagte: Rohdaten sind geil.

Später habe ich noch vollständige Datensätze bzw. die Rohdaten bekommen. Leider kann/möchte ich diese an dieser Stelle zum aktuellen Zeitpunkt nicht veröffentlichen, da es momentan keinen Weg gibt, die Dateistruktur von Saleae Logic zu verändern und ich somit die im Trace vorkommenden Seriennummern nicht entfernen kann.

Die Konfiguration für den SPI kann man größtenteils aus dem Datenblatt des Flashs ermitteln:

  • MSB first
  • 8 Bit je Datenwort
  • SPI-Mode 0 oder 3
  • CS low-aktiv

Im vorliegenden Fall wird Mode 0 verwendet.

Der Export des SPI-Analyzers aus Logic sieht wie folgt aus:

Time [s],Packet ID,MOSI,MISO
0.941889000000000,0,0xE8,0x00
0.941953000000000,0,0x00,0x00
0.942017416666667,0,0x00,0x00
0.942081583333333,0,0x00,0x00
0.942149166666667,0,0xAA,0x00
0.942213333333333,0,0xAA,0x00
0.942277583333333,0,0xAA,0x00
0.942341750000000,0,0xAA,0x00
0.942409833333333,0,0xAA,0x00
0.943974750000000,1,0xE8,0x00
0.944038833333333,1,0x00,0x00
0.944103083333333,1,0x00,0x00
0.944169166666667,1,0x00,0x00

...und ist damit ziemlich unleserlich. Neben den übertragenen Daten lässt sich auch die Bitrate grob abschätzen - jede Zeile entspricht einem übertragenen Byte und dauert im ersten Paket im Schnitt 65,1 µs. Mit 8 Bit je Datenwort kann ein Bit höchstens 8,138 µs dauern. Die Bitrate muss also etwas höher als 123 kHz sein, abhängig davon wie groß die Pausen zwischen den Bytes sind.

Die Wahrheit sieht natürlich etwas anders aus

233 kHz Clock-Frequenz und etwas um 32 µs zwischen den Bytes.

Traces lesbarer machen

Die Exporte von Logic sind gut, aber nicht für jede Anwendung. Um die übertragenen Daten zu verstehen ist mir das Timing nicht ganz so wichtig, aber der Inhalt der Nachrichten muss gut lesbar sein.

Also habe ich ein kleines Programm geschrieben (siehe unten), um die Pakete zu komprimieren, was dann wie folgt aussieht:

Time Packet Sent data Received data
0,941889000000000 0 E8 00 00 00 AA AA AA AA AA 00 00 00 00 00 00 00 00 00
0,943974750000000 1 E8 00 00 00 AA AA AA AA AA 00 00 00 00 00 00 00 00 00
...
7,979580416666670 14 E8 00 00 00 AA AA AA AA AA 00 00 00 00 00 00 00 00 AA
7,981663083333330 15 E8 00 00 01 AA AA AA AA AA 00 00 00 00 00 00 00 00 55
7,983659916666670 16 E8 00 00 02 AA AA AA AA AA 00 00 00 00 00 00 00 00 01
8,011756000000000 17 E8 00 0A 00 AA AA AA AA AA 00 00 00 00 00 00 00 00 FF
8,014754250000000 18 E8 00 00 00 AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA ... ...
8,051748750000000 19 82 00 00 00 AA 55 01 05 24 01 FF FF FF FF FF FF FF FF FF 00 20 20 57 61 67 6E 65 72 20 26 20 43 6F 20 00 ... ...

Zugleich wird der Timestamp angepasst, damit die Datei direkt von Excel eingelesen werden kann. Die Spalten sind zudem per Tab getrennt, was beim Import etwas angenehmer sein kann

Schaut man im Datenblatt nach den verschiedenen Befehlen, findet man unter 0xE8 "Continuous Array Read" und unter 0x82 "Main Memory Page Program through Buffer 1". Tabelle 4 im Dokument den Aufbau der zu den Befehlen gehörenden Sequenzen.

Demnach wird in Paket 0 der Zeiger auf Page 0 und Buffer Address Bit 0 gesetzt und ein Byte gelesen, welches mit 0x00 beantwortet wird. Ignoriert man Paket 1 geschieht dies im Sekundentakt (nicht oben gezeigt), bis in Paket 14 0xAA zurückgegeben wird.

Jetzt kann man die Kommunikation schon ziemlich gut verfolgen, allerdings: warum sollte man es sich unnötig schwer machen, wenn man den PC für sich arbeiten lassen kann? Also habe ich das Programm zum Komprimieren der Daten erweitert - es kann auch die im Protokoll verwendeten Befehle interpretieren:

Time Packet Opcode Page Address from/to Data length Data
0,941889000000000 0 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
0,943974750000000 1 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
...
7,979580416666670 14 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 AA
7,981663083333330 15 E8H Cont Array Read 0x0000 0x000001 ... 0x000001 1 55
7,983659916666670 16 E8H Cont Array Read 0x0000 0x000002 ... 0x000002 1 01
8,011756000000000 17 E8H Cont Array Read 0x0005 0x000500 ... 0x000500 1 FF
8,014754250000000 18 E8H Cont Array Read 0x0000 0x000000 ... 0x0000FF 256 AA 55 01 00 FF FF FF FF FF FF FF FF FF FF FF FF FF ...
8,051748750000000 19 82H Write through Buff1 0x0000 0x000000 ... 0x0000FF 256 AA 55 01 05 24 01 FF FF FF FF FF FF FF FF FF 00 20 ...

Kommunikation zum Speicher

Erkennung

Um den Datastick zu erkennen, wird im Sekundentakt versucht die Adresse 0x00000 zu lesen. Beinhaltet diese den Wert 0x00 wird davon ausgegangen, dass kein Stick gesteckt ist (keine Antwort). Wird das "Magic Byte" 0xAA erkannt, wird anschließend Adresse 0x00001 gelesen, die bei einem korrekt initialisierten Stick 0x55 beinhaltet:

Time Packet Opcode Page Address from/to Data length Data
5,970892583333330 11 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
6,974197916666670 12 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
6,976281000000000 13 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
7,979580416666670 14 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 AA
7,981663083333330 15 E8H Cont Array Read 0x0000 0x000001 ... 0x000001 1 55

Diese Erkennung läuft auch nachdem ein Stick erfolgreich erkannt wurde. Es ist der einfachste Weg, ein Entfernen des Loggers festzustellen, auch wenn es über die anderen Kommunikationsinhalte ebenfalls möglich wäre.

Anschließend gibt es eine Fallunterscheidung: Entweder der Stick kommt frisch initialisiert vom PC oder er war schon einmal an der Anlage.

Dazu wird an Adresse 0x000500 geprüft, ob 0xFF (mutmaßlich unbeschrieben) oder ein anderer Wert steht. Handelt es sich um ersteren, wird von einem frisch initialisierten Stick ausgegangen.

Frisch initialisierter Stick

Time Packet Opcode Page Address from/to Data length Data
5,970892583333330 11 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
6,974197916666670 12 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
6,976281000000000 13 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
7,979580416666670 14 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 AA
7,981663083333330 15 E8H Cont Array Read 0x0000 0x000001 ... 0x000001 1 55
7,983659916666670 16 E8H Cont Array Read 0x0000 0x000002 ... 0x000002 1 01
8,011756000000000 17 E8H Cont Array Read 0x0005 0x000500 ... 0x000500 1 FF
8,014754250000000 18 E8H Cont Array Read 0x0000 0x000000 ... 0x0000FF 256 AA 55 01 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ...
8,051748750000000 19 82H Write through Buff1 0x0000 0x000000 ... 0x0000FF 256 AA 55 01 05 24 01 FF FF FF FF FF FF FF FF FF 00 20 20 57 ...
8,095399500000000 20 82H Write through Buff1 0x0002 0x000200 ... 0x0002FF 256 20 20 20 20 44 61 74 75 6D 20 20 20 20 20 00 08 20 20 20 ...
8,139579833333330 21 82H Write through Buff1 0x0003 0x000300 ... 0x0003FF 256 20 20 41 75 73 67 61 6E 67 20 33 20 20 20 00 0A 20 20 41 ...
8,182810250000000 22 82H Write through Buff1 0x0004 0x000400 ... 0x0004FF 256 46 75 6E 6B 74 69 6F 6E 20 61 6B 74 69 76 00 0B 20 20 44 ...
8,224643666666670 23 E8H Cont Array Read 0x0000 0x000003 ... 0x000003 1 05
8,226721500000000 24 E8H Cont Array Read 0x0000 0x000003 ... 0x000003 1 05
8,229152833333330 25 81H Erase Page 0x0005 0x000500 ... 0x0005FF 0
8,239133916666670 26 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 AA
8,980954250000000 27 E8H Cont Array Read 0x0005 0x000500 ... 0x0005FF 256 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ...
9,018144083333330 28 82H Write through Buff1 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 ...
9,156107750000000 29 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 AA
9,158207083333330 30 E8H Cont Array Read 0x0000 0x000001 ... 0x000001 1 55
9,160199750000000 31 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 AA
9,991345666666670 32 E8H Cont Array Read 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 ...
10,028507166666700 33 82H Write through Buff1 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 ...

In diesem Fall steht an Adresse 0x000500 der Wert 0xFF (Paket 17), was auf noch nicht vorhandene Loggingdaten deutet.

In Paket 18 wird augenscheinlich geprüft, ob Kopfdaten vorhanden sind, anschließend werden die Systeminfo (Paket 19) und die Spaltenbeschreibungen (Paket 20, 21 und 22) geschrieben.

Die Bedeutung der zusätzlich geschriebenen Bytes (0x05 0x24 0x01) hat sich mir leider noch nicht erschlossen.

Warum Byte 0x000003 hier noch einmal und vor allem zweimal (Paket 23 und 24) abgefragt wird, ist mir ebenfalls etwas rätselhaft.

Ab Paket 25 beginnt der normale Schreibzyklus (siehe weiter unten).

Bereits verwendeter Stick

Time Packet Opcode Page Address from/to Data length Data
8,586632916666670 17 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
9,592280083333330 18 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
9,594365416666670 19 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 00
10,597658333333300 20 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 AA
10,599741083333300 21 E8H Cont Array Read 0x0000 0x000001 ... 0x000001 1 55
10,601737750000000 22 E8H Cont Array Read 0x0000 0x000002 ... 0x000002 1 0F
10,629873166666700 23 E8H Cont Array Read 0x0005 0x000500 ... 0x000500 1 6F
Gleicher Lesevorgang und Antwort in Paket 24 ... 50 für die Adressen

0x000540, 0x000580, 0x0005C0, 0x000600, 0x000640, 0x000680, 0x0006C0, 0x000700, 0x000740,

0x000780, 0x0007C0, 0x000800, 0x000840, 0x000880, 0x0008C0, 0x000900, 0x000940, 0x000980,

0x0009C0, 0x000A00, 0x000A40, 0x000A80, 0x000AC0, 0x000B00, 0x000B40, 0x000B80, 0x000BC0

10,692716666666700 51 E8H Cont Array Read 0x000C 0x000C00 ... 0x000C00 1 6F
10,694810333333300 52 E8H Cont Array Read 0x000C 0x000C40 ... 0x000C40 1 FF
10,696810750000000 53 E8H Cont Array Read 0x0000 0x000000 ... 0x000000 1 AA
11,599035166666700 54 E8H Cont Array Read 0x000C 0x000C00 ... 0x000CFF 256 6F 00 46 04 33 00 24 00 B0 01 D3 01 C4 09 C4 09 C4 09 C4 ...

Nach dem Lesen der Kopfdaten wird Adresse 0x000500, also der Beginn der Loggingdaten angesprungen und ein Byte gelesen (Paket 23). Ist dieses nicht 0xFF, wird so lange um 64 Byte weitergesprungen (Paket 24 ... 51), bis der erste nicht vorhandene Datensatz mit 0xFF am Anfang gefunden wird (Paket 53). An dieser Stelle kann der Schreibzyklus begonnen werden.

Schreibzyklus

Das Schreiben der Daten hätte ich mir eigenwilliger nicht ausmalen können. Aufgrund der Gegebenheiten ging ich von zwei Möglichkeiten aus:

  • Im Regler werden 4 Einträge gecached und anschließend als komplette Page geschrieben
  • Jeder Eintrag wird einzeln geschrieben und dabei der Pointer auf die jeweilige Adresse gesetzt

Stattdessen ist die Vorgehensweise wie folgt:

  1. Prüfen ob es der nächste zu schreibende Satz der letzte innerhalb der aktuellen Page ist und wenn ja: nächste Page löschen
  2. Aktuelle Page auslesen
  3. Aktuelle Page mit angehängten Daten wieder schreiben

Die folgende Tabelle ist leider etwas groß, aber hier benötigt man zum Verständnis die gelesenen/geschriebenen Daten. Zum besseren Übersicht habe ich die zyklischen Abfragen zur Erkennung des Sticks entfernt.

Time Packet Opcode Page Address from/to Data length Data
8,980954250000000 27 E8H Cont Array Read 0x0005 0x000500 ... 0x0005FF 256 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
9,018144083333330 28 82H Write through Buff1 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 09 42 02 3D 01 C4 09 C4 01 00 00 00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
9,991345666666670 32 E8H Cont Array Read 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 09 42 02 3D 01 C4 09 C4 01 00 00 00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
10,028507166666700 33 82H Write through Buff1 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 09 42 02 3D 01 C4 09 C4 01 00 00 00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2A 00 57 00 B6 01 DC 01 CB 08 CB 08 CB 08 CB 08 31 02 A0 01 E3 05 C3 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
10,994477083333300 37 E8H Cont Array Read 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 09 42 02 3D 01 C4 09 C4 01 00 00 00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2A 00 57 00 B6 01 DC 01 CB 08 CB 08 CB 08 CB 08 31 02 A0 01 E3 05 C3 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
11,031559916666700 38 82H Write through Buff1 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 09 42 02 3D 01 C4 09 C4 01 00 00 00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2A 00 57 00 B6 01 DC 01 CB 08 CB 08 CB 08 CB 08 31 02 A0 01 E3 05 C3 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2B 00 51 00 B5 01 DC 01 EB 08 EB 08 EB 08 EB 08 38 02 6D 01 D4 07 C3 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
12,001553583333300 42 81H Erase Page 0x0006 0x000C00 ... 0x000CFF 0
12,012532583333300 43 E8H Cont Array Read 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 09 42 02 3D 01 C4 09 C4 01 00 00 00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2A 00 57 00 B6 01 DC 01 CB 08 CB 08 CB 08 CB 08 31 02 A0 01 E3 05 C3 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2B 00 51 00 B5 01 DC 01 EB 08 EB 08 EB 08 EB 08 38 02 6D 01 D4 07 C3 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
12,049725916666700 44 82H Write through Buff1 0x0005 0x000500 ... 0x0005FF 256 6F 00 45 04 29 00 23 00 AD 01 D8 01 C4 09 C4 09 C4 09 C4 09 42 02 3D 01 C4 09 C4 01 00 00 00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2A 00 57 00 B6 01 DC 01 CB 08 CB 08 CB 08 CB 08 31 02 A0 01 E3 05 C3 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2B 00 51 00 B5 01 DC 01 EB 08 EB 08 EB 08 EB 08 38 02 6D 01 D4 07 C3 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 6F 00 45 04 2C 00 4C 00 B4 01 DB 01 EB 08 EB 08 EB 08 EB 08 3D 02 55 01 D4 07 C2 01 00 00 00 00 00 00 00 00 00 55 55 00 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00
13,007502916666700 48 E8H Cont Array Read 0x0006 0x000600 ... 0x0006FF 256 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
13,044694416666700 49 82H Write through Buff1 0x0006 0x000600 ... 0x0006FF 256 6F 00 45 04 2D 00 47 00 B3 01 DB 01 06 09 06 09 06 09 06 09 3F 02 47 01 CC 08 C2 01 00 00 00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00 F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF 00 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

Das ist auch schon so ziemlich alles.

Erwähnenswert ist an dieser Stelle noch, dass das Logging-Intervall am Regler während der Laufzeit verändert werden kann. In diesem Fall wird die erste Page neu geschrieben.

Flash-Emulator

Nun könnte man einfach dauerhaft mit eingestecktem Stick mitschnüffeln und die Daten so extrahieren. Leider hört der Solarregler (wahrscheinlich) mit dem Logging auf, wenn der Speicher voll ist. Besser ist da eine Lösung, die man anstecken und vergessen kann.

Eine Möglichkeit wäre, den SPI-Flash über Multiplexer an einen Mikrocontroller (oder direkt den SBC) zu hängen und immer wieder auszulesen. Geht, ist aber unnötig viel Aufwand und irgendwann, in 100 Jahren ist der Speicher totgeschrieben.

Die meisten Mikrocontroller besitzen einen SPI-Controller, der auch Slave kann. Mit ein bisschen Software lässt sich doch sicher der Speicher emulieren. Der komplette Befehlssatz ist nicht nötig, genauso wenig muss man den kompletten Speicherplatz des Originalbausteins vorhalten.

Also habe ich mir also einen ATmega328P (anfänglich ein ATmega8, der aufgrund der 3,3 V allerdings nicht stabil lief) auf ein Breadboard gesteckt und losgelegt - bzw. erst einmal blöd aus der Wäsche geschaut.

Zwar wird der Slave-Select-Pin zum aktivieren des SPI-Moduls verwendet, aber es gibt (zumindest beim ATmega8) keinen direkt assoziierten Interrupt auf Kommunikationsstart. Der wird aber zwingend benötigt, da man zur Synchronisation des Protokolls wissen muss, wann ein Paket angefangen hat (zurücksetzen des Byte-Zählers). Ungläubig und sehr irritiert habe ich beim ATmega8 Slave Select mit INT0 verbunden. Beim ATmega328P ist dies nicht notwendig, da dieser über Pin Change Interrupts verfügt, die auf nahezu allen Port-Pins verfügbar sind.

Für die Flash-Emulation werden also zwei Interrupts benötigt: SPI_STC (Serial Transfer Complete) und PCINT0 (dem der Slave-Select-Pin zugeordnet ist).

Der Programmablauf ist grob wie folgt:

Bei fallender Flanke am Slave Select wird, neben dem völlig autark ablaufenden Aktivieren des SPI-Moduls, der Byte-Zähler und der Op-Code zurückgesetzt und das SPDR-Datenregister mit 0x00 vorgeladen. Zusätzlich wird ein Busy-Flag gesetzt um - wenn die Firmware weiter wächst - Aufgaben niedrigerer Prioritäten zurückzustellen.

Bei steigender Flanke am Slave Select wird geprüft, welche Aktion im letzten Paket angefordert wurde, was vor allem beim Löschen der Page oder nach dem Schreiben der Loggingdaten - auf die wir es schließlich abgesehen haben - wichtig ist.

Im Serial SPI_STC-Interrupt passiert die eigentliche Arbeit: Wird ein Byte über SPI empfangen, schlägt es hier auf. Dank der Variable spi_bytenum weiß das Programm, wo es sich innerhalb des Pakets befindet.

Folglich wird das erste Byte in die Variable für den Op-Code geladen, die nächsten drei Byte landen im Adresszähler.

Um nicht unnötig Laufzeit zu verschwenden (Interrupt-Zeit ist als "teuer" zu betrachten), habe ich für die Behandlung der Adresse einen Trick (wenn man es so nennen will) angewandt: Der Wert ist 20 bzw. 21 Bit breit. Der nächsthöhere Standardtyp wäre uint32_t, der bei Rechenoperationen jedoch relativ viel Speicher frisst - schließlich handelt es sich um eine 8-Bit-CPU. Da größtenteils eh nur die unteren 16 Bit der Adresse wichtig sind, habe ich eine Union (also eine Verschmelzung mehrerer Datentypen) erstellt, die die Adresse etwas angenehmer speichern lässt:

volatile union
{
	struct
	{
		uint16_t lower;
		uint8_t upper;
	} val24;
	uint8_t val8[3];
} flash_address;

Das Schreiben der Werte sieht zwar etwas ungewöhnlich aus, orientiert die Bytes allerdings gleich richtig herum an und berücksichtigt auch den Offset von spi_bytenum (die Adresse kommt für spi_bytenum > 0 und < 4):

flash_address.val8[sizeof(flash_address) - spi_bytenum] = d;

Im Anschluss sind wir schon eine Ebene höher: Für den Op-Code 0xE8 (Read Continuous Array) wird es nach dem 7. Byte interessant, bei Write through Buffer 1 ab dem 4. Byte, was dann in etwa wie folgt aussieht:

ISR(SPI_STC_vect)
{
	IOPIN_H(DBGO);
	uint8_t d = SPDR;
	if(spi_bytenum == 0)
	{
		// byte 0 in transfer is the opcode
		SPDR = 0x00;
		flash_opcode = (Flash_Opcode_t)d;
	}
	else if(spi_bytenum < 4)
	{
		SPDR = 0x00;
		// byte 1 ... 3 are address bytes
		flash_address.val8[sizeof(flash_address) - spi_bytenum] = d;
		if(spi_bytenum == 3)
		{
			// command completely received, now do what needs to be done
			//uart_debug_write("%2x:%4x", flash_opcode, flash_address.val24.lower);
		}
	}
	else if(flash_opcode == Flash_Opcode_ReadContArray && spi_bytenum >= 7)
	{
		// when reading from the flash, we have to start to transmit
		// the data already one byte earlier due to delay of MISO
		if(flash_address.val24.lower <= 0x0003)
		{
			// hello bytes
			SPDR = flash_data_hello[flash_address.val24.lower & 0x03];
		}
		else if(flash_address.val24.lower >= 0x0500)
		{
			// logging data section
			SPDR = flash_data_log[flash_address.val24.lower & 0xFF];
		}
		flash_address.val24.lower++;
	}
	else if(flash_opcode == Flash_Opcode_WriteBuff1 && spi_bytenum >= 4)
	{
		if(flash_address.val24.lower >= 0x0500)
		{
			flash_data_log[flash_address.val24.lower & 0xFF] = d;
		}
		flash_address.val24.lower++;
	}
	else
	{
		// when there's no specific handler for the transmission, reply 0x00
		SPDR = 0x00;
	}
	
	// byte number is only important for the first n bytes, after that
	// there are anyway only data bytes (which are handled separately)
	if(spi_bytenum < 0xFF)
		spi_bytenum++;
	IOPIN_L(DBGO);
}

ISR(PCINT0_vect)
{
	if(IOPIN_V(SPI_SS) == 0)
	{
		spi_bytenum = 0;
		flash_opcode = 0;
		// preload SPI data register
		SPDR = 0x00;
		trx_flags |= TrxFlags_SpiBusy;
	}
	//...
}

Um die Laufzeit im Blick zu haben, wird zusätzlich ein Debug-Pin zu Beginn des Interrupts auf High und am Ende auf Low gesetzt. Wenn ich mich richtig erinnere, waren es bei 8 MHz CPU-Takt um die 8 µs, also reichlich Luft zu den knapp 30 µs, die zur Verfügung stehen. Allerdings muss dazu gesagt werden, dass in dem Codeschnipsel das Schreiben der Daten in den RAM noch nicht implementiert ist.

Livedaten

Denn in etwa an der Stelle, hatte ich das Geräusch vom Kratzen eines Plattenspielers im Kopf. Eine Mail von Frank mit neuen Loggingdaten. Ich hab zwar schon zuvor welche im Saleae-Format bekommen, nun aber mit etwas höherer zeitlicher Auflösung und so schaute ich sie mir noch einmal etwas genauer an:

Der Stick wurde etwa bei Sekunde 8 gesteckt. Abgesehen vom "Ping" an den (teilweise nicht vorhandenen) Stick ist da verdammt viel Gewackel auf SCK und MOSI, bei dem noch nicht einmal CS aktiv ist... Da wird doch wohl nicht...

In Logic kann man den Enable (also CS) auch auf active high einstellen. So kommt zu Tage, was in der Zeit über den Bus geht, das nicht den Flash betrifft.

Hier ein kleiner Auszug aus dem Block, der von ziemlich vielen 0xAA gefolgt wird (deswegen habe ich es mir vorher auch nicht genauer angesehen):

Time [s],Packet ID,MOSI,MISO
1.013882583333333,0,0xAA,0x00
1.013947000000000,0,0x55,0x00
1.014011416666667,0,0x55,0x00
1.014075666666667,0,0xAA,0x00
1.014139750000000,0,0x02,0x00
1.014203916666667,0,0x00,0x00
1.015365916666667,0,0xAA,0x00
1.015494333333333,0,0xAA,0x00
...

Irgendwann geht die Umsetzung vollkommen in die Hose, nachdem - warum weiß vermutlich nur der Hersteller - ein 11-bit langes Datenwort gesendet ist:

Nach dem nächsten Zugriff auf den Flash funktioniert die Analyse zwar wieder (da die State Machine zurückgesetzt wird) aber bis dahin hat man Datenschrott.

Nachdem man sich in Logic auch die Rohdaten exportieren lassen kann, gibt es keine Ausrede es nicht selbst besser zu machen.

Also einen sehr einfachen SPI-Analyzer in C# geschrieben, der das Timing sowohl für die Byte-Trennung als auch Pakettrennung verwendet. Kommt eine steigende Flanke am Clock später als 8 µs nach der letzten Veränderung, wird ein neuer Datensatz begonnen. Ist die Zeitdifferenz größer als 150 µs, wird ein neues Paket begonnen. Dabei sind die zeitlichen Abstände sehr konservativ.

Folgende Daten purzeln aus dem Programm:

Time CS Data
0,00850 1 AA 55 55 AA 02 00
0,00998 1 AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA
0,05851 1 AA 55 55 AA 01 00
0,05939 1 20 20 4B 6F 6C 6C 65 6B 74 6F 72 20 20 20 20 20 20 20 20 20 20 20 20 20 2D 31 34 20 05 2D 54 20 20 33 20 32 37 20 01 01 18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05
0,20858 1 AA 55 55 AA 02 00
0,21006 1 AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA
0,25859 1 AA 55 55 AA 01 00
0,25946 1 20 20 4B 6F 6C 6C 65 6B 74 6F 72 20 20 20 20 20 20 20 20 20 20 20 20 20 2D 31 34 20 05 2D 54 20 20 33 20 32 37 20 01 01 18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05
0,40867 1 AA 55 55 AA 02 00
0,41015 1 AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA
0,41492 1 09

Nachrichtentypen

Nach genauerem Durchsehen der Daten lassen sich 6 verschiedene Nachrichtentypen erkennen:

Länge Zyklus [ms] Meldungstyp Bezeichnung Anmerkung
1 474/531 - unbekannt immer 0x09, immer 11 bit lang
6 n/a - Meldungstyp Gibt an, welche Nachrichtentypen folgen
33 201 02 00 Remote Request (?) alle Bytes 0xAA, vermutlich Lesen eines abgesetzten Bedienteils
64 201 01 00 Displayanzeige ASCII, synchron zum Displayinhalt am Regler
68 1005 03 00 Messwerte Gleiches Datenformat wie auf dem Datastick
70 1005 03 01 Kopfdaten Spalteninformationen zu den Messwerten und Systeminfos
260 - - unbekannt vermutlich Systemparameter

Die Länge ist die Länge der verschiedenen Nachrichten. Der Meldungstyp der unterschiedlichen Nachrichten leitet sich aus der Nachricht Meldungstyp ab, die diese speziellen Nachrichten ankündigt.

0x09

Das 0x09 ist - wie bereits geschrieben - die Katze im Protokoll: Ist da, holt sich Aufmerksamkeit und wirft mutwillig Zeug auf den Boden, nur um die Welt brennen zu sehen. Oder bringt mit ihrer abweichenden Länge zumindest die State-Machine vom Hardware-SPI durcheinander.

Sinn und Zweck hat sich mir noch nicht erschlossen. Selbst wenn man das Timing betrachtet, taugt sie relativ wenig. Sie kommt abwechselnd in Abständen von etwa 474 und 531 ms, zwei dieser Nachrichten haben also einen Zyklus von 1,005 Sekunden. Könnte also der Synchronisation dienen.

Aber warum die komische Länge von 11 bit?

Meldungstyp

Diese Nachricht kündigt andere Nachrichten an. Die letzten beiden Byte geben - wie in der Tabelle oben vermerkt - den nächsten Meldungstyp an:

Offset(h) 00 01 02 03 04 05
00000000  AA 55 55 AA 01 00  ªUUª..

Das Timing ist bedingt durch die Natur der Nachricht nicht so richtig zyklisch, wobei sich natürlich eine Regelmäßigkeit erkennen lässt.

Remote Request

Auch wenn ich es nicht mit sicher sagen kann, handelt es sich hierbei mit großer Wahrscheinlichkeit um eine Abfrage eines abgesetzten Bedienteils. Auf eine andere Weise kann ich mir nicht erklären, warum nur 0xAA gesendet wird (was übrigens auch bei Lesevorgängen im Flash gesendet wird).

Ein weiterer Fürsprecher ist, dass die Nachricht durchschnittlich nur 50 ms vor der Übertragung der Displayanzeige gesendet wird, was sehr für die Optimierung Aktion <> Reaktion spricht.

Leider sind mir über die Inhalte keine näheren Informationen bekannt.

Displayanzeige

Bei dieser Nachricht handelt es sich mit sehr großer Wahrscheinlichkeit um den Inhalt eines zusätzlichen Displays. Nach den zwei Byte Kopfdaten kommen 36 ASCII-Zeichen. Da das Display am Regler 3 Zeilen mit 14 + 10 + 14 Zeichen hat, ist die Auftrennung der Daten vermutlich (!) dementsprechend.

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  20 20 57 61 67 6E 65 72 20 26 20 43 6F 20 20 20    Wagner & Co   
00000010  20 20 20 20 20 20 20 20 20 53 6F 6C 61 72 74 65           Solarte
00000020  63 68 6E 69 6B 20 01 00 00 00 01 00 00 00 10 00  chnik ..........
00000030  00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 65  ...............e

  • Zeile 1 auf dem Display
  • Zeile 2 auf dem Display
  • Zeile 3 auf dem Display
  • Weitere Daten

Die weiteren Daten haben sich mir leider nicht erschlossen, scheinen aber Parameter für die Anzeige zu sein. Zum Beispiel anzuzeigende Symbole oder das Mapping von Informationen wie der Uhrzeit (wie hier gezeigt), die nicht in der Nachricht übertragen wurde, aber auf dem Display zu sehen ist.

Das letzte Zeichen: siehe Prüfsumme

Messwerte

Die Messdaten sind nahezu identisch zu dem, was auf dem Datastick landet:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  20 00 6F 00 45 04 21 00 24 00 AD 01 D8 01 C4 09   .o.E.!.$...Ø.Ä.
00000010  C4 09 C4 09 C4 09 45 02 40 01 C4 09 CD 01 00 00  Ä.Ä.Ä.E.@.Ä.Í...
00000020  00 00 00 00 00 00 00 55 55 10 00 00 00 00 00 00  .......UU.......
00000030  F8 08 00 00 01 00 01 00 01 00 02 00 00 00 F0 FF  ø.............ðÿ
00000040  00 00 20 C9                                      .. É

Die Daten werden durch die grün und gelb markierten Bytes eingeschlossen, die konstant zu sein scheinen. Das letzte Zeichen ist wieder die Prüfsumme.

Kopfdaten

Wie beim Logging auch werden Kopfdaten ausgegeben. Aufgrund der Gesamtlänge werden sie in kleinere Blöcke unterteilt und in diesen übertragen.

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  0D 11 04 00 20 20 20 20 44 61 74 75 6D 20 20 20  ....    Datum   
00000010  20 20 08 01 20 20 20 55 68 72 7A 65 69 74 20 20    ..   Uhrzeit  
00000020  20 20 09 02 20 53 65 6B 75 6E 64 65 6E 20 00 00    .. Sekunden ..
00000030  00 00 10 03 20 20 4B 6F 6C 6C 65 6B 74 6F 72 20  ....  Kollektor 
00000040  20 20 01 00 24 28                                  ..$(

  • Konstant
  • Adresse
  • Daten
  • Trennzeichen
  • Prüfsumme

Ich bin mir nicht ganz sicher, ob die Aufteilung Konstant/Adresse so korrekt ist, es wäre schon fast naheliegender, wenn die Aufteilung 2/2 Byte wäre. Allerdings habe ich keinen Anhaltspunkt dafür entdeckt.

Die Adressen nach dem angegebenen Muster schlüsseln sich wie folgt auf:

Adresse Bezeichnung
0x00 Spaltenbeschreibung 0
0x04 Spaltenbeschreibung 1
0x04 * n Spaltenbeschreibung n
0x20 Spaltenbeschreibung 8
0xFA Systemdaten

Spaltenbeschreibungen

Fügt man die Daten aus den Nachrichten zusammen (hier: Adresse 0x00 und 0x04), erkennt man einen Unterschied zur Struktur im Flash:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  20 20 20 20 44 61 74 75 6D 20 20 20 20 20 08 01      Datum     ..
00000010  20 20 20 55 68 72 7A 65 69 74 20 20 20 20 09 02     Uhrzeit    ..
00000020  20 53 65 6B 75 6E 64 65 6E 20 00 00 00 00 10 03   Sekunden ......
00000030  20 20 4B 6F 6C 6C 65 6B 74 6F 72 20 20 20 01 00    Kollektor   ..
00000040  20 20 53 70 65 69 63 68 65 72 20 B9 20 20 01 05    Speicher ¹  ..
00000050  20 20 53 70 65 69 63 68 65 72 20 B3 20 20 01 06    Speicher ³  ..
00000060  20 52 FC 63 6B 6C 61 75 66 61 6E 68 2E 20 01 07   Rücklaufanh. ..
00000070  20 52 FC 63 6B 6C 61 75 66 61 6E 68 2E 20 01 00   Rücklaufanh. ..

Wo das vorletzte Byte jeder Spaltenbeschreibung 0x00 war, steht hier der Spaltentyp. Dafür wird hier das letzte Byte hochgezählt, wobei das letzte einer Nachricht immer 0x00 ist. Trotzdem wird die Zählung in der nächsten Nachricht korrekt fortgesetzt. Bug oder Feature?

Systemdaten

Auch die Systemdaten sind etwas anders aufgebaut als im Datastick:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000  20 20 57 61 67 6E 65 72 20 26 20 43 6F 20 00 FB    Wagner & Co
00000010  20 53 6F 6C 61 72 74 65 63 68 6E 69 6B 20 00 FC   Solartechnik
00000020  XX XX XX XX XX XX XX XX XX XX 00 00 00 00 00 FD  XXXXXXXXXX.....ý
00000030  XX XX XX XX XX XX 00 XX XX XX XX 00 00 00 00 00  XXXXXX.XXXX.....

  • Systemname
  • Seriennummer
  • System-Nummer
  • System-Version

Achtung: Im Gegensatz zum Datenstick sind hier System-Nummer und System-Version nicht Little-Endian, sondern Big-Endian.

Systemparameter

Um ehrlich zu sein: ich habe nicht die leiseste Ahnung was der der Inhalt der Nachricht auch nur bedeuten könnte. Da ich nicht ausschließen kann, dass Informationen darin sind, die die Anlage identifizieren könnten, möchte ich an dieser Stelle nicht näher darauf eingehen.

Prüfsumme

Am Ende einer jeder Nutzdaten-Nachricht (also alle außer Meldungstyp und der verrückten 0x09) befindet sich eine Prüfsumme.

Diese lässt sich gut an den Nachrichten erkennen, deren Inhalte sich nur wenig ändern - das letzte Byte ist dabei immer anders. Da es sich nur um ein Byte handelt ging ich im ersten Momentan davon aus, dass es sich am ehesten um CRC-8 handelt. Also RevEng heruntergeladen und aufgrund der Uhrzeit dann lieber doch noch einmal geschaut, ob es sich die Entwickler nicht doch einfacher gemacht haben. Schließlich haben so manche eine Vorliebe, das mit der Summe wörtlich zu nehmen.

Warum nicht mal JavaScript?

var msg = "20 00 6F ... C9";
var bytes = msg.split(/ /);
var sum = 0;
for (var i = 0; i < bytes.length; i++)
{
  bytes[i] = parseInt(bytes[i], 16);
  if (i < bytes.length - 1)
  {
    sum += bytes[i];
  }
}
var csok = (sum & 255) == bytes[bytes.length - 1];
console.log('checksum is %sok', csok ? '' : 'not ');

Gleich beim ersten Schuss getroffen. Schön. Oder auch nicht - die Bytewerte zu summieren ist alles andere als sicher. Allerdings muss man auch eingestehen, dass CRC-8 und CRC-16 bei den Datenlängen auch nicht mehr zur zuverlässigen Fehlererkennung taugt.

Angezapft

Ok, jetzt wissen wir, wie die Daten aufgebaut sind. Jetzt müssen sie nur noch extrahiert und umgesetzt werden.

Um überhaupt Daten empfangen zu können, muss Slave Select auf Masse liegen (zumindest wenn die Daten kommen) - sonst ist das SPI-Modul inaktiv. Leider stellte sich heraus, dass der Regler für den "zweiten Kanal" keinen Chip select herausführt. Das hätte die Sache etwas vereinfacht.

Nun kann man einfach den Pin mit GND verbinden und bekommt die komplette Buskommunikation mit oder man invertiert das Signal - im einfachsten Fall mit einem Transistor - und hat schon mal die komplette Kommunikation zum Datenstick gefiltert (sofern man ihn überhaupt noch verwenden will).

Aufgrund des verrückten 0x09 muss das SPI-Modul im laufenden Betrieb zurückgesetzt werden. Am besten funktioniert das über das Timing der Nachrichten. Der kleinste Abstand den ich in den Traces zwischen ihnen gesehen habe, war um die 600 µs. Der zeitliche Abstand zwischen den Bytes immer unter 100 µs, Ticks um die 100 bis 250 µs würden also vollkommen reichen, um Nachrichten sauber unterscheiden zu können.

Zuerst habe ich mir überlegt, eine Statemachine zu verwenden, aber das ist gar nicht nötig, da es im Endeffekt nur zwei Zustände gibt: Es werden Daten empfangen und der Empfang ist abgeschlossen.

Als Schnittstelle zum restlichen Programm dienen drei Methoden:

  • prozeda_rxData
  • prozeda_tick
  • prozeda_task

prozeda_rxData muss durch den SPI-Received-Interrupt mit den empfangenen Daten als Parameter aufgerufen werden. In der Methode wird das empfangene Byte in den Puffer geschrieben und der Ticks-Zähler zurückgesetzt.

prozeda_tick erwartet einen Aufruf alle ca. 100 µs. Ein bisschen mehr oder weniger ist nicht schlimm, evtl. muss PROZEDA_MSG_MAXTICKS angepasst werden. Die Konstante sollte nicht kleiner als 3 werden und die dadurch eingefasste Zeit nicht zu nah am die knapp 600 µs kommen, da sonst die empfangenen Daten überschrieben werden. Im Kern überprüft die Methode, wie lange das letzte empfangene Byte her ist. Wurde etwas empfangen und die zeitliche Bedingung erfüllt, wird das Flag prozeda_process gesetzt und damit prozeda_task zur Ausführung freigegeben.

prozeda_task ist relativ unkritisch, sollte aber so regelmäßig ausgeführt werden dass es zwischen dem letzten prozeda_tick und dem nächsten prozeda_rxData noch vollständig durchläuft. Falls es in der Anwendung nicht reicht, kann ein doppelter Puffer helfen, der die Daten in prozeda_tick wegkopiert bevor sie von prozeda_rxData überschrieben werden können.

Hardware

AVR
Typ AtMega328P
Takt 12 MHz
Fuses
High 0x0xD9
Low 0x0xCE
Extended 0x0xFF
Engbedded com logo.png Details

Die Hardware ist relativ einfach: Der Atmega, sein Oszillator und der FET zum Invertieren des Chip Select. Dazu noch ein Angstwiderstände und fertig ist der Lack. Etwas schöner wäre natürlich nur noch eine passende Mini-DIN-Buchse.

Timing

Um herauszufinden, wie viel CPU-Last der Empfang und das Auswerten des Protokolls benötigt und was man sonst noch machen kann, ist es unabdingbar, das Timing zu messen. Nun kann man anhand des Listings und der Befehlstabelle Zyklen zählen oder messen.

Das geht mit einem Oszilloskop oder Logic Analyzer relativ einfach, indem man im Mikrocontroller am Anfang und Ende eines Funktionsaufrufs einen IO-Pin ein- bzw. ausschaltet.

Über den Duty-Cycle kann man dann die CPU-Last messen, sollte den Wert aber trotzdem mit etwas Vorsicht genießen, denn: Schaltet man den IO innerhalb einer Funktion, misst man nicht den Overhead für den Sprung in und aus derselben. Zum Beispiel fallen beim TIMER0_COMOA_vect beim Eintritt 4 push-Befehle, ein in- und ein eor-Befehl an. Plus dem Setzen des Pins: 12 Zyklen. Beim Austritt 4x pop, je einmal out und reti und cbi: 15 Zyklen. Bei 12 MHz CPU-Takt bleiben also ca. 2,25 µs "ungesehen". An ungeeigneter Stelle kann das unangenehm werden.

Folgende Zeiten habe ich mit dem Logic Analyzer gemessen - die Zeiten sind Mittelwerte aus 10 Messungen, der LA lief mit 32 MHz, also ist die zeitliche Auflösung 0,03125 µs

Methode Bezeichnung Dauer [µs]
prozeda_tick keine Kommunikation 0,588
prozeda_tick während Kommunikation 1,338
prozeda_tick 4 Zyklen nach Kommunikation 1,844
prozeda_rxData - 1,419
prozeda_process nach MsgType-Nachricht 8,918
prozeda_process nach Paket != Logdata 2,339
prozeda_process nach 0x09 2,339
prozeda_process nach Logdata 110,75

Die ganzen Werte sind ohne die oben erwähnten Ein- und Aussprungsszeiten.

Über Duty-Cycle-Messungen kann man prinzipiell auch die CPU-Load für die einzelnen Aufgaben ermitteln. Hier im Durchschnitt über ca. 20 Sekunden und wieder ohne den Overhead:

Methode CPU-Zeit [%]
prozeda_tick 0,968
prozeda_rxData 0,025
prozeda_process 0,101

Erstaunlicherweise erzeugt der Tick am meisten Auslastung und wäre damit der erste Kandidat für Optimierung.

Implementierungsstatus

Aktuell Werden lediglich die MsgType-Nachrichten und die Logdaten ausgewertet. Aufgrund mangelnder Erkenntnis werden die Displaydaten nicht verarbeitet. Bei den Spaltendaten ist der Aufbau zwar bekannt, da sie für den aktuellen Regler aber keine zusätzliche Verwendung finden, habe ich es ausgelassen (die Definition - enum und typedef struct - existieren allerdings schon). Auch die 260-Byte lange Nachricht wird nicht untersucht, was aber noch interessant wäre.

Ein weiterer Punkt wäre die Untersuchung des Rückkanals, um den Regler fernsteuern zu können. Besonders in Hinblick auf Fernwartung hätte dies einen größeren Reiz. Im Vordergrund stand in diesem Artikel allerdings das Auslesen der Messwerte.

Programmbeispiel

Im Download befindet sich ein Programmbeispiel, das gegen die Kommunikation des RATIOfresh 200 getestet wurde. Nach dem Empfang werden die Logdaten (Datum/Uhrzeit, Temperaturen, Ausgänge, Funktionen und Zapfung) als Text mit 115200 Baud 8N1 am UART ausgegeben.

Neben der Ausgabe als "normalem" Text können die Daten auch im JSON-Format ausgegeben werden.

Für die Anbindung an den Raspberry Pi, oder andere Embedded Systems wäre auch ein I²C-Slave sehr praktisch (Vielleicht, wenn ich lustig bin...). Ebenso eine Portierung auf einen physisch kleineren Mikrocontroller.

Anmerkungen

  • Vielen Dank an Frank für die vielen Infos, Daten, Fotos und Geduld!
  • Vielen Dank an das Forum von mikrocontroller.net und im speziellen Rainer B.
  • Für die Stimulation des Mikrocontrollers wurde sowohl ein anderer Mikrocontroller als auch ein zweiter Logic(-Nachbau) missbraucht. Besonders letzteres funktioniert erstaunlich gut.
  • Es gibt nun eine Implementierung für den Raspberry Pi: Pirozeda

Downloads

Datei:Prozeda-Decoder.zip enthält:

  • Hexdump eines Flashimages
  • Excel-Datei zur Auswertung eines Logging-Eintrags + Datenbeschreibung
  • Atmel Studio-Projekt zur Emulation des Datasticks
  • Atmel Studio-Projekt mit dem Reader der Livedaten