Eine Kamera mit einem eher unterdimensionierten Mikrocontroller
Disclaimer: Dieses Projekt entstand hauptsächlich an einem Wochenende. Sehr viel Dokumentation ist außer dem Quelltext daher nicht vorhanden. Wer diese Kamera nachbauen möchte (und an einen der verwendeten Thermodruckköpfe gelang ist), muss sich mit der Information aus pinning.h begnügen und sich selbst um Pegelwandler und Schrittmotortreiber kümmern.
Ich hatte vier dieser Thermodruckermodule, die ich vor Jahren gekauft hatte, weil sie billig und interessant waren. Jahrelang lagen sie also in einer Schublade und verspotteten mich. Etwas musste getan werden!
Die beschriebenen Module sind LTP1245s von SII. Volle Dokumentation ist damit verfügbar (sonst hätte ich sie auch nicht gekauft). Elektrisch gesehen brauchen sie einen Treiber für den Schrittmotor, einen ADC für den Heißleiter zur Temperaturmessung und einige 5 V-Signale zur Steuerung. Der Betriebsbereich ist flexibel genug für einen Betrieb an einem 2s-LiIon-Akkupack. Alles in allem nicht zu viel Aufwand für die Ansteuerung.
Nun aber, was macht man mit einem Thermodrucker? Ich erinnerte mich dunkeln an ein Video von mikeselectricstuff über günstige CMOS-Kameramodule an Mikrocontrollern. Direkt entstand der Plan, ein LTP1245-Thermodruckermodul über einen der beliebten STM32F103C8T6 mit einem OV7670-Kameramodul zu bedienen.
Die Wahl fiel auf diesen Mikrocontroller, da ich keine eigene Platine erstellen und ätzen wollte, um die Zeit mit der Software verbringen zu können. Alle Development-/Evaluationboards mit leistungsstärkeren Controllern, die ich hatte, waren deutlich größer als die klassische Blue Pill und damit zu groß für meine Idee der Thermodrucker-Selfiekamera. Also fing ich mit diesem Controller an, auch um zu sehen, wie weit ich damit kommen würde. Es stellte sich dann tatsächlich als komplizierter als gedacht heraus, aus den folgenden Gründen:
- Der STM32F103 hat (natürlich) kein Kamerainterface. Allerdings hat er auch keinen Speichercontroller, welcher im oben verlinkten Video verwendet wird.
- 20 KiB RAM sind gar nicht so viel, wenn es um Bildverarbeitung geht.
- Das war vermutlich das erste mal, dass mir bei einem STM32 die Timer ausgingen.
Ausgabe
Der erste Schritt war der Druckerteil. Obwohl ein LTP1245 nicht sehr viel externe Beschaltung benötigt, ist er intern auch nicht besonders komplex. Außer dem schrittmotorgetriebenen Papiereinzug enthält er:
- Ein Schieberegister für die Pixeldaten mit einem Ausgangslatch.
- Einen Leistungstransistor für jedes Pixel einer Zeile.
- Zwei Gabellichtschranken zur Erkennung der Druckkopfposition und zur Papiererkennung.
- Ein Heißleiter zur Messung der Druckkopftemperatur.
Um eine Zeile zu drucken, werden die Daten ins Schieberegister getaktet und die Heizelemente über die Latches für eine bestimmte Zeit aktiviert. Anschließend wird das Papier mittels Schrittmotor um einen Schritt weiterbewegt und der Prozess wiederholt sich. Die Aktivierungszeit kann anhand von der Versorgungsspannung, der Druckkopftemperatur und weniger Konstanten für das verwendete Papier berechnet werden. Dadurch wird eine definierte Energie in die Beschichtung des Papiers übertragen und wiederholbare Druckqualität erreicht.
Die Druckgeschwindigkeit ist hauptsächlich durch den Schrittmotor begrenzt (und steigt daher auch mit höherer Versorgungsspannung). Als Treiber kommen zwei Emitterfolger-Paare pro Phase zum Einsatz. Nachdem der Motor keine hohe Leistung erfordert und nicht einmal Microstepping nötig ist, besteht kein Bedarf für ein eigenes Treiber- oder Steuerungs-IC und diese Lösung ist mehr als ausreichend.
Softwareseitig landen Temperaturmessung, Papiereinzug und zeilenweises Drucken in einer Zustandsmaschine. Neben dem Bilddrucken implementierte ich auch eine einfache Textausgabe, die zwar am Ende nicht gebraucht wurde, aber beim Debuggen hilfreich war. Zudem kann der Controller noch ein PWM-Signal erzeugen um ein Servo für einen Papierschneider anzusteuern (was letztendlich auch nicht gebraucht wurde).
Damit ist die Ausgabe auf Papier abgedeckt. Mit einem funktionierenden Drucker konnte ich mich dem Kamera-Teil widmen.
Eingabe
Der OV7670 hat einen langsamen I²C-Bus für die Konfiguration und einen schnelleren Parallelbus für die Bilddaten. Der Takt für letzteren wird vom Kameramodul auf Basis eines Eingangstakts erzeugt. Nachdem es keinen Puffer gibt und er direkt vom Frame-Timing abhängt, ist er relativ frei konfigurierbar.
Nachdem der Controller kein Kamerainterface hat und nur wenig RAM verfügbar ist, musste ich Bildauflösung und Pixeltakt so weit wie möglich herunterstellen. Nachdem der Thermodrucker nur 384 Pixel pro Zeile hat, hilft eine geringere Bildauflösung auch eher als dass sie stört. Die Dokumentation des OV7670 sagt nicht ganz eindeutig, wie die verschiedenen Einstellungen sich gegenseitig beeinflussen, daher entstand die Konfiguration hauptsächlich durch Herumprobieren. Ich landete letztendlich bei diesen Einstellungen:
// Disable timing resets
WriteRegister(REG_COM6, 0x00);
// Set clock prescaler to 2
WriteRegister(REG_CLKRC, 0x4 | 1);
// Enable scaling
WriteRegister(REG_COM3, 0x08);
// Use QCIF output format
WriteRegister(REG_COM7, 0x08);
// Blank pixel clock during sync pulses
WriteRegister(REG_COM10, 0x20);
// Enable pixel clock scaling
WriteRegister(REG_COM14, 0x18 | 1);
WriteRegister(REG_SCALING_PCLK_DIV, 1);
Das Kameramodul spuckt nun Daten aus, der Mikrocontroller muss sie aber noch aufnehmen. Das geschieht wie folgt:
- VSYNC ist mit einem Input-Capture-Pin eines Timers verbunden und löst ein Interrupt für jeden Puls aus. Somit wird zu Beginn jedes Frames ein Zeilenzähler auf Null zurückgesetzt bzw. die Datenerfassung beendet sobald ein brauchbares Bild im Speicher ist (es wird ja nur eines gebraucht).
- Auf ähnliche Weise wird mit einem anderen Timer ein HSYNC-Interrupt erzeugt. Der Interrupt-Handler erhöht den Zeilenzähler und rekonfiguriert den DMA.
- Zu guter Letzt löst der Pixeltakt über einen Input-Capture-Pin einen DMA-Request aus. Der DMA liest von den LSBs von Port B (die mit den Datenleitungen verbunden sind) und schreibt die empfangenen Daten ins RAM. Die RAM-Adresse wird danach erhöht, sodass eine komplette Zeile vollautomatisch eingelesen wird.
Der Pixeltakt kann so konfiguriert werden, dass er während der Synchronisationspulse aussetzt. Dadurch gibt es nur Taktflanken für echte Pixel und keine überflüssigen Daten werden aufgezeichnet.
Bildverarbeitung
Es bleiben somit 160 auf 144 Pixel Bilddaten. Wer mitrechnet, wird bemerken, dass ein komplettes Frame 22.5 KiB hat und damit alleine schon größer als das verfügbare RAM wäre. Damit wird auch verständlich, weshalb das HSYNC-Interrupt nicht übersprungen und ein ganzes Frame auf einmal eingelesen werden kann.
Glücklicherweise ist die HSYNC-Pause lang genug für grundlegende Bildverarbeitung. Jede Zeile wird daher mittels Dithering zu schwarz-weiß konvertiert, was den Speicherbedarf auf ein Achtel reduziert. Zu Beginn verwendete ich ein einfaches 1D-Dithering, später den Floyd-Steinberg-Algorithmus. Nachdem mir das 1D-Dithering ästhetisch gut gefiel, ließ ich es als Option im Code.
Sobald ein komplettes Frame eingelesen und in ein Schwarzweißbild konvertiert wurde, kann die Aufnahme gestoppt und das Bild gedruckt werden. Nach Abschluss des Drucks schaltet die Kamera ihre eigene Stromzufuhr mittels eines p-Kanal-MOSFETs ab und wartet auf die nächste Aktivierung.
Luft nach oben
An diesem Punkt war mir die Kamera gut genug, und sie hat sich auch als unterhaltsamer Party-Gimmick bewährt. Einige Möglichkeiten zur Verbesserung:
- Durch den geringen Pixeltakt sind Rolling-Shutter-Effekte sehr deutlich. Wenn die Kamera während der Aufnahme bewegt wird, sieht man sie trotz geringer Auflösung und nachfolgender Bildverarbeitung.
- Das verwendete Kameramodul hat einen doch sehr kleinen Blickwinkel. Für einen Selfie muss man die Kamera mindestens auf Armlänge halten. Ohne Sucher ist es auch nicht einfach von jemandem oder etwas ein Bild zu machen.
- Die Software kompensiert Unter- oder Überbelichtungen in keiner Weise. Stattdessen wird eine mehr oder minder korrekte Belichtung angenommen. Nachdem es nicht genug RAM für eine Korrektur über ein gesamtes Bild gibt, wäre diese ohnehin nur über mehrere Frames hinweg möglich. Aktuell steht nur die Belichtungsautomatik des Kameramoduls zwischen veränderlichem Umgebungslicht und komplett schwarzen oder weißen Ausdrucken. In der Tat schien die beste Lösung zu sein, einige Frames abzuwarten, bevor das Bild aufgenommen wird. Nachdem die Kamera nur Strom bekommt, wenn ein Photo geschossen werden soll, ist die verfügbare Zeit aber begrenzt.
Weniger flüchtige Bilder (Update Juli 2020)
Nachdem es die Kamera nun eine Weile gibt, sie aber immer noch ab und an verwendet wird, wollte ich sie noch um ein letztes Feature erweitern. Die oben aufgelisteten Probleme sind großteils prinzipbedingt und ohne eine Generalüberholung des Designs schwer lösbar. Insofern war die Idee eine andere: Eine klassischere Ausgabemethode als Kassenbonpapier.
Eigentlich sollte die Kamera von Anfang an einen SD-Kartenslot bekommen. Das scheiterte zunächst aber daran, dass am Mikrocontroller keine SPI-Pins mehr frei waren: Ein SPI-Peripheral wurde für die Pixelausgabe an den Thermodrucker gebraucht und der Parallelbus für die Kamera belegte zu viele Pins, egal wie ich die Belegung anzupassen versuchte.
Damals hatte ich allerdings nicht gesehen, dass es zu FatFs ein äußerst einfach zu portierendes Programmbeispiel mit bit-banging SPI gibt. Dass das Abspeichern damit langsamer wird als mit einem Hardware-SPI steht außer frage, aber wenn das Bild nach dem Ausdrucken, bevor sich die Kamera selbst abschaltet, abgespeichert wird, kann das zugegebenermaßen so lange dauern wie es will, ohne dass die Verzögerung auffällt. Damit ist vor allem kein Hardware-SPI mehr nötig und alles was ich brauchte waren 4 beliebige freie Pins.
Vier Pins reichen, denn den „card detect“-Schalter ließ ich unverwendet. Wenn kein Speichermedium gesteckt ist, schlägt beim Speicherversuch eines Bildes das Einhängen fehl und die Kamera schaltet sich ab ohne eine Datei abzulegen. Nachdem es keine weitere Benutzerinteraktion gibt, hätte es auch keinen Vorteil die Präsenz einer SD-Karte zu erkennen.
Ich verwendete einen der für einen externen 32 kHz-Quarz vorgesehenen Pins und schuf Raum für den Rest, indem ich einige Features entfernte, die sowieso nicht verwendet wurden (zum Beispiel die Status-LED und die Servo-Ansteuerung). Der Quarz kann auf der blue pill bleiben, denn er stört die digitalen Signale nicht besonders.
Um nun auch Dateien ablegen zu können, schrieb ich eine rudimentäre BMP-Ausgaberoutine, die das geditherte Photo aus dem Arbeitsspeicher auf der SD-Karte abspeichert. Nachdem die Bilder eine ziemlich kleine Auflösung haben, rein zweifarbig sind und wegen des Ditherings kaum uniforme Bereiche haben, ist BMP sogar fast optimal (und nicht nur einfach zu generieren). Eine Bitmapdatei bietet zwar keinen Raum für Metadaten, allerdings gäbe es auch kaum welche, die man abspeichern könnte.
Zu guter Letzt braucht es noch einen Dateinamen. Dafür testet die Firmware alle Dateien nach dem Muster TCIM-nnn.BMP
(für thermal camera image, in Anspielung auf den typischen Ordner DCIM) und erhöht die Zahl nnn
am Ende, bis sie einen Namen findet, unter dem noch keine Datei existiert. Dadurch bleibt die Aufnahmereihenfolge der Bilder erhalten ohne dass irgendwo im Flash noch ein Zähler abgespeichert werden müsste (außer natürlich man löscht zwischendurch Photos von der SD-Karte). Zwar kann es etwas dauern, bis ein freier Dateiname gefunden wird, aber wie zuvor angemerkt ist das Abspeichern ja nicht zeitkritisch.
Natürlich sehen die Digitalphotos kaum vergleichbar zu den ausgedruckten aus. Dennoch denke ich dass sie auch in digitaler Form, immer noch dieselben Bilder mit geringer Auflösung und kleinem Farbraum, einen guten Teil ihres Charmes behalten können. Die Möglichkeit einer digitalen Kopie ist damit eine strikte Verbesserung, schließlich kann man die Kamera nach wie vor in ihrer ursprünglichen Form verwenden – ohne eine Aufzeichnung abseits des doch recht unbeständigen Kassenbonpapier-Ausdrucks zu hinterlassen.
Links
Der Sourcecode findet sich auf GitHub.