LED-Matrix lisp-arduino-interface
Unser Ziel ist, Animationen auf der einer 16×16 Matrix von LEDs mit WS2811 Treibern darzustellen.
Ein Repo fuer den aktuellen Code ist auf https://github.com/plops/rgb_led_matrix.
Der folgende verkuerzte Aussschnitt soll nur einen groben Ueberblick geben.
Der Arduino liest jedes Byte, dass er vom seriellen Port bekommt in einen Speicher. Das Byte mit dem Wert 254 hat eine besondere Funktion. Wenn es empfangen wird, wird ein interner Zaehler zurueckgesetzt, so dass das Bild geschrieben werden kann.
- led-ctl.ino
#include "Adafruit_NeoPixel.h" #define PIN 6 enum { WIDTH=16, HEIGHT=16, N=WIDTH*HEIGHT*3 }; Adafruit_NeoPixel strip = Adafruit_NeoPixel(WIDTH * HEIGHT, PIN, NEO_GRB + NEO_KHZ800); void setup() { strip.begin(); Serial.begin(115200); strip.setPixelColor(0, strip.Color(10,10,10)); strip.show(); // Funktionstest: zeige einen einzigen Pixel an } int counter = 0; char buf[3*WIDTH*HEIGHT]; void loop() { int incomingByte = 0; while((Serial.available() > 0) && (incomingByte != -1)) { incomingByte = Serial.read(); if (incomingByte != -1) { // Lesefehler gibt -1 if(incomingByte == 254) counter = 0; else{ buf[counter] = incomingByte; counter++; if (counter >= N) counter = 0; } } } if (counter >= N-1) { for (int i = 0; i < WIDTH*HEIGHT; i++) { strip.setPixelColor(i, strip.Color(buf[3*i], buf[3*i+1], buf[3*i+2])); } strip.show(); } }
Zum Experimentieren mit verschiedenen Bildern, habe ich Clozure Common Lisp auf dem Raspberry PI installiert. Die Installation wird hier beschrieben: http://www.raspihub.com/go/f5780dbf11dabc60771e67b357ae947bc6b3fd87f35d5f38e7d511ff88e08d0c
Der folgende Code oeffnet den seriellen Port zum Arduino, sendet das Byte 254 und dann 256*3 Bytes mit Bildinformationen (in diesem Fall werden zwei diagonale Linien angezeigt).
(load "/home/pi/ccl/library/serial-streams.lisp") (defparameter *a* (ccl::make-serial-stream "/dev/ttyUSB0" :format :binary :baud-rate 115200 :parity nil :char-bits 8 :stop-bits 1 :flow-control nil)) (let* ((a1 (make-array (* 3 256) :element-type '(unsigned-byte 8))) (a (make-array (list 16 16 3) :element-type '(unsigned-byte 8) :displaced-to a1))) (dotimes (c 3) (dotimes (j 16) (dotimes (i 16) (let ((r (sqrt (+ (* i i) (* j j))))) (setf (aref a j i c) ;; x (if (or (= i j) (= (- 16 i) j)) 34 0)))))) (write-byte 254 *a*) (write-sequence a1 *a*) nil)
Diskussion
Ab und zu hatte ich das Probleme, dass mein Lisp abgestuerzt ist. Ich vermute, dass das verwendete Raspbian zu viele Prozesse gestartet hat (ich hatte insbesonder XWindow, einen Browser und die java-basierte Arduino Entwicklungsumgebung auf). Dadurch war vielleicht der RAM knapp. Die Javaumgebung laesst sich leicht vermeiden, denn sie zeigt alle Aufrufe des AVR Compilers an und eine entsprechende Makefile kann leicht erzeugt werden.
Ich habe auch Lisp Code geschrieben um einen 16×16 Font von einem PGM Bild einzulesen. Die Bildrate ist (gefuehlt) 4 Hz, also nicht genug um Scrollenden Text anzuzeigen.
Das naechste Mal werde ich vermutlich einen weiteren Modus auf dem Arduino implementieren, um Spalten mit monochromen Bilddaten ueber die Matrix zu schieben. Auf diese Weise sollte fluessiger Text moeglich werden.
Wenn der Lisp Code funktioniert und zufriedenstellende Animationen liefter, kann die Logik fuer vereinfachtes Deployment in ein C-Programm uebertragen werden.
Alternative Ansteuerung des LED Arrays
Ich bin mir sicher, dass der oben erwqaehnte Ansatz funktionieren wird. Er hat aber zwei erhebliche Nachteile. Zum einen muss ich viel C auf dem Arduino programmieren (was keinen Spass macht) und zum anderen ist die Bildqualitaet sehr eingeschraenkt.
Deshalb habe ich mir ueberlegt, ob man das LED Array direkt mit dem Raspberry PI ansteuern kann. Der Pi kommt mit zwei UARTS und einer SPI Schnittstelle. Ich bin zu den Schluss gekommen, dass man mit einem UART ein Signal erzeugen kann, das die WS2811 Chips verstehen sollten.
Ziel ist es ein 800kHz Rechteck-Signal zu erzeugen, in dem jeder Puls ein Bit kodiert. Ein Puls mit Duty Cycle < 0.5 steht fuer WS_LOW und ein Puls mit Duty Cycle > 0.5 steht fuer WS_HIGH.
Ich betreibe den UART mit 8MHz Baudrate. Das Signal sieht dann so aus:
-........--........--........-
Hierbei steht ein Punkt „.“ fuer eines der 8 Datenbits und eine Minus „-“ fuer die Start und Stopbits. Diese muessen leider immer mitgesendet werden. Dies ist jedoch nur fuer das erste gesendete serielle Datenpacket problematisch, bei allen darauf folgenden Bits formen das Stopbit des vorangehenden Packets und das neue Startbit den Anfang des Pulses. Je nach Datenbyte kann man dann einstellen, wie der WS2811 das Packet interpretiert:
-........--........--........- .xxxxxyyyyy.
Hier habe ich die 10 seriellen Bits (beginnend mit Start und Stopbits) in die Klassen „x“ und „y“ eingeteilt. Die „x“ bilden einen Puls mit Duty Cycle < 0.5. Werden auch ein paar der „y“ aktiviert, ist der Duty Cycle > 0.5 und der WS2811 interpretiert die eingehende Pulsform als WS_HIGH.
-........--........--........- .xxxxxyyyyy. .1110000000. WS_LOW .1111111000. WS_HIGH
Ein Byte mit der Bitfolge #b10000000=#x80 steht also fuer WS_LOW und ein Byte mit #b1111100=#xf8 steht fuer WS_HIGH.
Im Falle des ersten Packets koennte es nuetzlich sein, die Bitfolgen #b11000000=#xc0 bzw. #b11111100=#xfc senden, um das nichtvorhandene Stopbit zu kompensieren.
Ich habe bereits die /boot/config.txt und /etc/inittab in meinem Pi geaendert, so dass der primaere UART0 /dev/ttyAMA0 nicht mehr als Konsole benutzt wird.
Ausserdem habe ich gelesen, dass die UART_CLOCK standardmaessig auf nur 3MHz beschraenkt ist und die Baudrate ist auf ein 16tel dieses Takts beschraenkt. Deshalb habe ich die UART_CLOCK auf 160MHz gesetzt, bin aber nicht sicher ob das funktioniert hat. Mit stty kann ich bisher keine baudraten groesser als 4MHz einstellen.
Ich werde versuchen, den Takt mit einem Oszilloskop zu messen und wenn noetig die Divider in den Registern des ARM Chips zu manipulieren. Vielleicht geht das, ohne dass der Linuxkernel das mitbekommt.
Falls das nicht geht, genuegen vielleicht auch 4 MHz.
-........--........--........- .xxxxxyyyyy. .xxxyyaaabb. .1100011000. WS_LO WS_LO .1111011110. WS_HI WS_HI x1100011110x WS_LO WS_HI x1111011000x WS_HI WS_LO
In diesem Fall wuerde ein Byte zwei Pulse und zwei Bits fuer den WS2811 erzeugen. #xb00011000 fuer zweimal LOW, #xb11011110 fuer zweimal HIGH usw. Die Messung mit dem Oszilloskop ergibt, dass das Startbit immer 0v zeigt, das Stopbit hingegen 3.3v. Weiterhin werden die Datenbytes beginnend vom niedrigsten Bit ausgesendet.
L12345678H _--------^_--------^_--------^
Trotzdem kann man aus diesem Signal nach wie vor die Waveform bilden, die der WS2811 verstehen sollte. Das TxD Signal muss invertiert werden, so dass Startbits 3.3v werden. Die restlichen 8 Datenbits muessen so gesetzt werden, dass zwei Pulse mit der gewuenschten Laenge entstehen. D.h. fuer jeden Puls stehen 5 Bit zur Verfuegung. Sind 4 High, zaehlt er als WS_HIGH. Ist nur einer High, zaehlt er als WS_LOW.
^--------_^--------_^--------_ eeeccaaxxxyyybbddfff _----_--___ HI LO #b00110111 _____--___----_ LO HI #b11110001 _____--___--___ LO LO #b00110001 _____----_----_ HI HI #b11110111