===== LED-Matrix lisp-arduino-interface =====
{{ hswiki:archiv:jensites:projekte:laufend:led-matrix:rgb-led-matrix-circle.jpg?300}}
Unser Ziel ist, Animationen auf der einer 16x16 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.
#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 16x16 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