In diesem Artikel möchte ich über die Funktionen der Implementierung einer grafischen Benutzeroberfläche mit Widgets auf einem Mikrocontroller sprechen und darüber, wie man sowohl eine vertraute Benutzeroberfläche als auch eine anständige FPS hat. Achtung Ich möchte mich nicht auf eine bestimmte Grafikbibliothek konzentrieren, sondern auf allgemeine Dinge - Speicher, Prozessor-Cache, DMA und so weiter. Da ich Entwickler des Embox- Teams bin, beziehen sich die angegebenen Beispiele und Experimente auf dieses RT-Betriebssystem.
Wir haben bereits darüber gesprochen , die Qt-Bibliothek auf einem Mikrocontroller auszuführen . Die Animation erwies sich als recht flüssig, aber die Speicherkosten selbst für das Speichern der Firmware waren erheblich - der Code wurde aus dem externen QSPI-Flash-Speicher ausgeführt. Wenn eine komplexe und multifunktionale Schnittstelle erforderlich ist, die auch weiß, wie man eine Art Animation erstellt, können die Kosten für Hardwareressourcen durchaus gerechtfertigt sein (insbesondere, wenn Sie diesen Code bereits für Qt entwickelt haben).
Aber was ist, wenn Sie nicht alle Funktionen von Qt benötigen? Was ist, wenn Sie vier Tasten, einen Lautstärkeregler und ein paar Popup-Menüs haben? Gleichzeitig möchte ich, dass es „gut aussieht und schnell arbeitet“ :) Dann ist es ratsam, leichtere Werkzeuge zu verwenden, zum Beispiel die lvgl- Bibliothek oder ähnliches.
In unserem Embox-Projekt wurde vor einiger Zeit Nuklear portiert - ein Projekt zum Erstellen einer sehr einfachen Bibliothek, die aus einem Header besteht und es Ihnen ermöglicht, auf einfache Weise eine einfache GUI zu erstellen. Wir haben uns entschlossen, damit eine kleine Anwendung zu erstellen, in der es ein Widget mit einer Reihe grafischer Elemente gibt, das über einen Touchscreen gesteuert werden kann.
Als Plattform wurde STM32F7-Discovery mit Cortex-M7 und Touchscreen ausgewählt.
Erste Optimierungen. Speicher speichern
So wird die Grafikbibliothek ausgewählt, ebenso die Plattform. Lassen Sie uns nun verstehen, was die Ressourcen sind. Es ist erwähnenswert, dass der Hauptspeicher-SRAM um ein Vielfaches schneller ist als der externe SDRAM. Wenn die Bildschirmgrößen dies zulassen, ist es natürlich besser, den Framebuffer in den SRAM zu legen. Unser Bildschirm hat eine Auflösung von 480x272. Wenn wir eine Farbe von 4 Bytes pro Pixel wollen, erhalten wir ungefähr 512 KB. Gleichzeitig beträgt die Größe des internen RAM nur 320 und es ist sofort klar, dass der Videospeicher extern sein wird. Eine andere Möglichkeit besteht darin, die Farbbittiefe auf 16 (d. H. 2 Bytes) zu reduzieren und somit den Speicherverbrauch auf 256 KB zu reduzieren, was bereits in den Haupt-RAM passen kann.
Das erste, was Sie versuchen können, ist, alles zu sparen. Lassen Sie uns einen 256-KB-Videopuffer erstellen, ihn in den Arbeitsspeicher legen und darin zeichnen. Das Problem, auf das wir sofort stießen, war das „Flackern“ der Szene, das beim direkten Zeichnen in den Videospeicher auftritt. Nuklear zeichnet die gesamte Szene von Grund auf neu. Jedes Mal, wenn der gesamte Bildschirm zuerst ausgefüllt wird, wird dann das Widget gezeichnet, dann wird eine Schaltfläche eingefügt, in die der Text eingefügt wird, und so weiter. Dadurch kann das bloße Auge sehen, wie die gesamte Szene neu gezeichnet wird und das Bild „blinkt“. Das heißt, eine einfache Platzierung im internen Speicher wird nicht gespeichert.
Zwischenpuffer. Compiler-Optimierungen. FPU
Nach einigem Hin und Her mit der vorherigen Methode (Platzierung im internen Speicher) kamen mir sofort Erinnerungen an X Server und Wayland in den Sinn. Ja, tatsächlich bearbeiten Fenstermanager Anfragen von Kunden (nur unsere benutzerdefinierte Anwendung) und sammeln dann die Elemente in der endgültigen Szene. Beispielsweise sendet der Linux-Kernel Ereignisse von Eingabegeräten über den evdev-Treiber an den Server. Der Server bestimmt wiederum, welcher Client das Ereignis adressieren soll. Kunden, die ein Ereignis erhalten haben (z. B. durch Drücken auf einen Touchscreen), führen ihre interne Logik aus - sie markieren die Schaltfläche und zeigen ein neues Menü an. Weiter (etwas anders für X und Wayland) zeichnet entweder der Client selbst oder der Server die Änderungen in den Puffer. Und dann setzt der Komponist alle Teile zusammen, um sie auf dem Bildschirm zu zeichnen.Einfach genug und schematische Erklärung hierhier .
Es wurde klar, dass wir eine ähnliche Logik benötigen, aber wir wollen X Server wirklich nicht für eine kleine Anwendung in stm32 verschieben. Versuchen wir daher, nicht im Videospeicher, sondern im normalen Speicher zu zeichnen. Nach dem Rendern der gesamten Szene wird der Puffer in den Videospeicher kopiert.
Widget-Code
if (nk_begin(&rawfb->ctx, "Demo", nk_rect(50, 50, 200, 200),
NK_WINDOW_BORDER|NK_WINDOW_MOVABLE|
NK_WINDOW_CLOSABLE|NK_WINDOW_MINIMIZABLE|NK_WINDOW_TITLE)) {
enum {EASY, HARD};
static int op = EASY;
static int property = 20;
static float value = 0.6f;
if (mouse->type == INPUT_DEV_TOUCHSCREEN) {
/* Do not show cursor when using touchscreen */
nk_style_hide_cursor(&rawfb->ctx);
}
nk_layout_row_static(&rawfb->ctx, 30, 80, 1);
if (nk_button_label(&rawfb->ctx, "button"))
fprintf(stdout, "button pressed\n");
nk_layout_row_dynamic(&rawfb->ctx, 30, 2);
if (nk_option_label(&rawfb->ctx, "easy", op == EASY)) op = EASY;
if (nk_option_label(&rawfb->ctx, "hard", op == HARD)) op = HARD;
nk_layout_row_dynamic(&rawfb->ctx, 25, 1);
nk_property_int(&rawfb->ctx, "Compression:", 0, &property, 100, 10, 1);
nk_layout_row_begin(&rawfb->ctx, NK_STATIC, 30, 2);
{
nk_layout_row_push(&rawfb->ctx, 50);
nk_label(&rawfb->ctx, "Volume:", NK_TEXT_LEFT);
nk_layout_row_push(&rawfb->ctx, 110);
nk_slider_float(&rawfb->ctx, 0, &value, 1.0f, 0.1f);
}
nk_layout_row_end(&rawfb->ctx);
}
nk_end(&rawfb->ctx);
if (nk_window_is_closed(&rawfb->ctx, "Demo")) break;
/* Draw framebuffer */
nk_rawfb_render(rawfb, nk_rgb(30,30,30), 1);
memcpy(fb_info->screen_base, fb_buf, width * height * bpp);
In diesem Beispiel wird ein 200 x 200 Pixel großes Fenster erstellt und Grafiken werden darin gezeichnet. Die letzte Szene selbst wird in den Puffer fb_buf gezogen, den wir dem SDRAM zugewiesen haben. Und dann wird in der letzten Zeile memcpy einfach aufgerufen. Und alles wiederholt sich in einem endlosen Kreislauf.
Wenn wir dieses Beispiel nur erstellen und ausführen, erhalten wir ungefähr 10-15 FPS. Was sicherlich nicht sehr gut ist, weil es auch mit dem Auge auffällt. Da der Nuklear-Rendercode viele Gleitkommaberechnungen enthält, haben wir seine Unterstützung zunächst aktiviert , ohne ihn wäre der FPS sogar noch niedriger gewesen. Die erste und einfachste (kostenlose) Optimierung ist natürlich das -O2-Compiler-Flag.
Lassen Sie uns dasselbe Beispiel erstellen und ausführen - wir erhalten 20 FPS. Besser, aber immer noch nicht genug für einen guten Job.
Prozessor-Caches aktivieren. Durchschreibemodus
Bevor wir zu weiteren Optimierungen übergehen, möchte ich sagen, dass wir das rawfb-Plugin als Teil von Nuklear verwenden, das direkt in den Speicher zieht. Dementsprechend sieht die Speicheroptimierung sehr vielversprechend aus. Das erste, was mir in den Sinn kommt, ist der Cache.
In älteren Versionen von Cortex-M, wie z. B. Cortex-M7 (unser Fall), ist ein zusätzlicher Prozessor-Cache (Anweisungs-Cache und Daten-Cache) integriert. Sie wird über das CCR-Register des System Control Block aktiviert. Mit der Aufnahme des Caches treten jedoch neue Probleme auf - die Inkonsistenz der Daten im Cache und im Speicher. Es gibt verschiedene Möglichkeiten, den Cache zu verwalten, aber in diesem Artikel werde ich nicht weiter darauf eingehen, daher werde ich meiner Meinung nach zu einer der einfachsten übergehen. Um das Problem der Cache- / Speicherinkonsistenz zu lösen, können wir einfach den gesamten verfügbaren Speicher als "nicht zwischenspeicherbar" markieren. Dies bedeutet, dass alle Schreibvorgänge in diesen Speicher immer in den Speicher und nicht in den Cache verschoben werden. Wenn wir jedoch den gesamten Speicher auf diese Weise markieren, hat der Cache auch keinen Sinn. Es gibt noch eine andere Option. Dies ist ein Durchgangsmodus, bei dem alle als durchgeschrieben gekennzeichneten Schreibvorgänge in den Speicher gleichzeitig an den Cache gesendet werden.und in Erinnerung. Dies verursacht einen Schreibaufwand, beschleunigt jedoch das Lesen erheblich, sodass das Ergebnis von der jeweiligen Anwendung abhängt.
Für Nuklear erwies sich der Durchschreibemodus als sehr gut - die Leistung stieg von 20 FPS auf 45 FPS, was an sich schon recht gut und flüssig ist. Der Effekt ist natürlich interessant. Wir haben sogar versucht, den Durchschreibemodus zu deaktivieren, ohne auf Dateninkonsistenz zu achten, aber der FPS stieg nur auf 50 FPS, dh es gab keinen signifikanten Anstieg im Vergleich zum Durchschreiben. Daraus folgerten wir, dass unsere Anwendung viele Lesevorgänge und keine Schreibvorgänge erfordert. Die Frage ist natürlich, wo? Möglicherweise aufgrund der Anzahl der Transformationen im rawfb-Code, die häufig auf den Speicher zugreifen, um den nächsten Koeffizienten oder ähnliches zu lesen.
Doppelte Pufferung (bisher mit einem Zwischenpuffer). DMA aktivieren
Ich wollte nicht bei 45 FPS anhalten, also beschlossen wir, weiter zu experimentieren. Die nächste Idee war die doppelte Pufferung. Die Idee ist allgemein bekannt und im Allgemeinen einfach. Wir zeichnen die Szene mit einem Gerät in einen Puffer, während das andere Gerät aus einem anderen Puffer angezeigt wird. Wenn Sie sich den vorherigen Code ansehen, sehen Sie deutlich eine Schleife, in der die Szene zuerst in den Puffer gezeichnet wird und der Inhalt dann mit memcpy in den Videospeicher kopiert wird. Es ist klar, dass memcpy die CPU verwendet, dh das Rendern und Kopieren erfolgt nacheinander. Unsere Idee war, dass das Kopieren parallel mit DMA erfolgen kann. Mit anderen Worten, während der Prozessor eine neue Szene zeichnet, kopiert der DMA die vorherige Szene in den Videospeicher.
Memcpy wird durch folgenden Code ersetzt:
while (dma_in_progress()) {
}
ret = dma_transfer((uint32_t) fb_info->screen_base,
(uint32_t) fb_buf[fb_buf_idx], (width * height * bpp) / 4);
if (ret < 0) {
printf("DMA transfer failed\n");
}
fb_buf_idx = (fb_buf_idx + 1) % 2;
Hier wird fb_buf_idx eingegeben - der Index des Puffers. fb_buf_idx = 0 ist der vordere Puffer, fb_buf_idx = 1 ist der hintere Puffer. Die Funktion dma_transfer () verwendet Ziel, Quelle und eine Anzahl von 32-Bit-Wörtern. Dann wird der DMA mit den erforderlichen Daten belastet und die Arbeit mit dem nächsten Puffer fortgesetzt.
Nachdem dieser Mechanismus ausprobiert wurde, stieg die Leistung auf etwa 48 FPS. Etwas besser als memcpy (), aber nur geringfügig. Ich möchte nicht sagen, dass sich DMA als nutzlos herausgestellt hat, aber in diesem Beispiel war die Auswirkung des Caches auf das Gesamtbild besser.
Nach einer kleinen Überraschung, dass DMA schlechter als erwartet abschnitt, kamen wir auf eine „ausgezeichnete“ Idee, wie es uns damals schien, mehrere DMA-Kanäle zu verwenden. Was ist der Sinn? Die Anzahl der Daten, die auf stm32f7xx gleichzeitig in DMA geladen werden können, beträgt 256 KB. Denken Sie gleichzeitig daran, dass unser Bildschirm 480 x 272 Pixel groß ist und der Videospeicher etwa 512 KB groß ist. Dies bedeutet, dass Sie die erste Hälfte der Daten in einen DMA-Kanal und die zweite Hälfte in den zweiten Kanal einfügen können. Und alles scheint gut zu sein ... Aber die Leistung sinkt von 48 FPS auf 25-30 FPS. Das heißt, wir kehren zu der Situation zurück, in der der Cache noch nicht aktiviert wurde. Womit kann es verbunden werden? Aufgrund der Tatsache, dass der Zugriff auf den SDRAM-Speicher synchronisiert ist, wird sogar der Speicher als synchroner dynamischer Direktzugriffsspeicher (SDRAM) bezeichnet, sodass diese Option nur zusätzliche Synchronisierung hinzufügt.ohne das Schreiben in den Speicher wie gewünscht parallel zu machen. Nach einigem Nachdenken haben wir festgestellt, dass es hier nichts Überraschendes gibt, da es nur einen Speicher gibt und die Schreib- und Lesezyklen für eine Mikroschaltung (auf einem Bus) generiert werden. Da eine andere Quelle / ein anderer Empfänger hinzugefügt wird, dann der Arbiter, der die Anrufe auf dem Bus auflöst müssen Sie Befehlszyklen von verschiedenen DMA-Kanälen mischen.
Doppelte Pufferung. Arbeiten mit LTDC
Das Kopieren aus einem Zwischenpuffer ist sicherlich gut, aber wie wir herausgefunden haben, reicht dies nicht aus. Schauen wir uns eine weitere offensichtliche Verbesserung an - die doppelte Pufferung. In den meisten modernen Display-Controllern können Sie die Adresse auf den verwendeten Videospeicher einstellen. Auf diese Weise können Sie das Kopieren ganz vermeiden und die Videospeicheradresse einfach in den vorbereiteten Puffer umordnen, und der Bildschirmcontroller nimmt die Daten über DMA auf die optimale Weise für sich allein auf. Dies ist eine echte Doppelpufferung ohne Zwischenpuffer wie zuvor. Es gibt auch eine Option, wenn der Display-Controller zwei oder mehr Puffer haben kann, was im Wesentlichen dasselbe ist - wir schreiben in einen Puffer und der andere wird vom Controller verwendet, während kein Kopieren erforderlich ist.
Der LTDC (LCD-TFT-Display-Controller) in stm32f74xx verfügt über zwei Hardware-Overlay-Schichten - Schicht 1 und Schicht 2, wobei Schicht 2 Schicht 1 überlagert ist. Jede der Schichten ist unabhängig konfigurierbar und kann separat aktiviert oder deaktiviert werden. Wir haben versucht, nur Schicht 1 zu aktivieren und die Videospeicheradresse im vorderen oder hinteren Puffer neu anzuordnen. Das heißt, wir geben eine an die Anzeige und zeichnen die andere zu diesem Zeitpunkt. Beim Wechseln von Overlays ist jedoch ein deutlicher Jitter aufgetreten.
Wir haben die Option ausprobiert, wenn wir beide Ebenen mit einer von ihnen ein / aus verwenden, dh wenn jede Ebene ihre eigene Videospeicheradresse hat, die sich nicht ändert, und der Puffer durch Einschalten einer der Ebenen geändert wird, während die andere ausgeschaltet wird. Die Variation führte auch zu Jitter. Und schließlich haben wir die Option ausprobiert, als die Ebene nicht ausgeschaltet war, der Alphakanal jedoch entweder auf Null 0 oder auf Maximum (255) eingestellt war. Das heißt, wir haben die Transparenz gesteuert und eine der Ebenen unsichtbar gemacht. Aber diese Option entsprach nicht den Erwartungen, das Zittern war immer noch vorhanden.
Der Grund war nicht klar - die Dokumentation besagt, dass Layer-Status-Updates im laufenden Betrieb durchgeführt werden können. Wir machten einen einfachen Test - wir schalteten die Caches und den Gleitkomma aus, zeichneten ein statisches Bild mit einem grünen Quadrat in der Mitte des Bildschirms, das für Ebene 1 und Ebene 2 gleich war, und begannen, die Ebenen in einer Schleife zu wechseln, in der Hoffnung, ein statisches Bild zu erhalten. Aber wir haben wieder den gleichen Shake bekommen.
Es wurde klar, dass es etwas anderes war. Und dann erinnerten wir uns an die Ausrichtung der Framebuffer-Adresse im Speicher. Da die Puffer vom Heap zugewiesen wurden und ihre Adressen nicht ausgerichtet waren, haben wir ihre Adressen um 1 KB ausgerichtet - wir haben das erwartete Bild ohne Jitter erhalten. Dann stellten sie in der Dokumentation fest, dass LTDC Daten in Stapeln von 64 Bytes subtrahiert und dass die Ungleichmäßigkeit der Daten zu einem erheblichen Leistungsverlust führt. In diesem Fall müssen sowohl die Adresse des Anfangs des Framebuffers als auch seine Breite ausgerichtet sein. Zum Testen haben wir die Breite von 480 x 4 in 470 x 4 geändert, die nicht durch 64 Byte teilbar ist, und den gleichen Jitter erhalten.
Infolgedessen haben wir beide Puffer um 64 Bytes ausgerichtet, sichergestellt, dass die Breite auch um 64 Bytes ausgerichtet ist, und Nuklear ausgeführt - der Jitter ist verschwunden. Die Lösung, die funktioniert hat, sieht so aus. Verwenden Sie Transparenz, anstatt zwischen Ebenen zu wechseln, indem Sie entweder Ebene 1 oder Ebene vollständig deaktivieren. Das heißt, um die Ebene zu deaktivieren, setzen Sie ihre Transparenz auf 0 und aktivieren Sie sie - auf 255.
BSP_LCD_SetTransparency_NoReload(fb_buf_idx, 0xff);
fb_buf_idx = (fb_buf_idx + 1) % 2;
BSP_LCD_SetTransparency(fb_buf_idx, 0x00);
Wir haben 70-75 FPS! Viel besser als das Original 15.
Es sollte beachtet werden, dass die Lösung durch Transparenzkontrolle funktioniert und die Optionen zum Deaktivieren einer der Ebenen und die Option zum Neuanordnen der Ebenenadresse den Bildjitter bei FPS groß 40-50 verursachen, der Grund ist uns derzeit unbekannt. Außerdem werde ich sagen, dass dies eine Lösung für dieses Board ist.
Hardware-Szenenfüllung über DMA2D
Dies ist jedoch nicht die Grenze. Unsere letzte Optimierung zur Erhöhung der FPS ist das Füllen der Hardware-Szene. Vorher haben wir das Füllen programmgesteuert durchgeführt:
nk_rawfb_render(rawfb, nk_rgb(30,30,30), 1);
Lassen Sie uns nun dem rawfb-Plugin mitteilen, dass die Szene nicht gefüllt, sondern nur übermalt werden muss:
nk_rawfb_render(rawfb, nk_rgb(30,30,30), 0);
Wir werden die Szene mit der gleichen Farbe 0xff303030 füllen, nur in Hardware über den DMA2D-Controller. Eine der Hauptfunktionen von DMA2D ist das Kopieren oder Füllen eines Rechtecks im RAM. Der Hauptvorteil hierbei ist, dass dies kein kontinuierlicher Speicher ist, sondern ein rechteckiger Bereich, der sich mit Unterbrechungen im Speicher befindet, was bedeutet, dass gewöhnliches DMA nicht sofort durchgeführt werden kann. In Embox haben wir noch nicht mit diesem Gerät gearbeitet. Verwenden Sie daher einfach die STM32Cube-Tools - die Funktion BSP_LCD_Clear (uint32_t Color). Es programmiert die Füllfarbe und -größe des gesamten Bildschirms in DMA2D.
Vertikale Austastperiode (VBLANK)
Aber selbst bei 80 FPS blieb ein spürbares Problem bestehen - Teile des Widgets bewegten sich mit kleinen „Pausen“, wenn sie sich über den Bildschirm bewegten. Das heißt, das Widget schien in drei (oder mehr) Teile unterteilt zu sein, die sich nebeneinander bewegten, jedoch mit einer leichten Verzögerung. Es stellte sich heraus, dass der Grund ein falsches Update des Videospeichers war. Genauer gesagt, Aktualisierungen in den falschen Zeitintervallen.
Der Display-Controller hat eine Eigenschaft wie VBLANK, es handelt sich auch um VBI oder Vertical Blanking Period . Es bezeichnet das Zeitintervall zwischen benachbarten Videobildern. Genauer gesagt die Zeit zwischen der letzten Zeile des vorherigen Videobilds und der ersten Zeile des nächsten. In diesem Intervall werden keine neuen Daten auf das Display übertragen, das Bild ist statisch. Aus diesem Grund ist es sicher, den Videospeicher in VBLANK zu aktualisieren.
In der Praxis verfügt der LTDC-Controller über einen Interrupt, der so konfiguriert ist, dass er nach der Verarbeitung der nächsten Framebuffer-Zeile (LTDC-Zeilen-Interrupt-Positionskonfigurationsregister (LTDC_LIPCR)) ausgelöst wird. Wenn Sie diesen Interrupt also auf die letzte Zeilennummer konfigurieren, erhalten wir nur den Anfang des VBLANK-Intervalls. An dieser Stelle nehmen wir die notwendige Pufferumschaltung vor.
Infolge solcher Aktionen normalisierte sich das Bild wieder, die Lücken waren verschwunden. Gleichzeitig fiel der FPS von 80 auf 60. Lassen Sie uns verstehen, was der Grund für dieses Verhalten sein könnte.
Die folgende Formel finden Sie in der Dokumentation :
LCD_CLK (MHz) = total_screen_size * refresh_rate,
Dabei ist total_screen_size = total_width x total_height. LCD_CLK ist die Frequenz, mit der der Display-Controller Pixel aus dem Videospeicher auf den Bildschirm lädt (z. B. über die Display Serial Interface (DSI)). Die Aktualisierungsrate ist jedoch bereits die Aktualisierungsrate des Bildschirms selbst, seine physikalische Eigenschaft. Wenn Sie die Aktualisierungsrate des Bildschirms und seine Abmessungen kennen, können Sie die Frequenz für den Display-Controller konfigurieren. Nachdem wir die Register auf die vom STM32Cube erstellte Konfiguration überprüft hatten, stellten wir fest, dass der Controller auf einen 60-Hz-Bildschirm eingestellt ist. Also kam alles zusammen.
Ein wenig über Eingabegeräte in unserem Beispiel
Kehren wir zu unserer Anwendung zurück und schauen wir uns an, wie der Touchscreen funktioniert, denn wie Sie verstehen, impliziert eine moderne Benutzeroberfläche Interaktivität, dh Interaktion mit dem Benutzer.
Hier ist alles ganz einfach arrangiert. Ereignisse von Eingabegeräten werden unmittelbar vor dem Rendern der Szene in der Hauptprogrammschleife verarbeitet:
/* Input */
nk_input_begin(&rawfb->ctx);
{
switch (mouse->type) {
case INPUT_DEV_MOUSE:
handle_mouse(mouse, fb_info, rawfb);
break;
case INPUT_DEV_TOUCHSCREEN:
handle_touchscreen(mouse, fb_info, rawfb);
break;
default:
/* Unreachable */
break;
}
}
nk_input_end(&rawfb->ctx);
Die Behandlung von Ereignissen über den Touchscreen erfolgt in der Funktion handle_touchscreen ():
handle_touchscreen
static void handle_touchscreen(struct input_dev *ts, struct fb_info *fb_info,
struct rawfb_context *rawfb) {
struct input_event ev;
int type;
static int x = 0, y = 0;
while (0 <= input_dev_event(ts, &ev)) {
type = ev.type & ~TS_EVENT_NEXT;
switch (type) {
case TS_TOUCH_1:
x = normalize_coord((ev.value >> 16) & 0xffff, 0, fb_info->var.xres);
y = normalize_coord(ev.value & 0xffff, 0, fb_info->var.yres);
nk_input_button(&rawfb->ctx, NK_BUTTON_LEFT, x, y, 1);
nk_input_motion(&rawfb->ctx, x, y);
break;
case TS_TOUCH_1_RELEASED:
nk_input_button(&rawfb->ctx, NK_BUTTON_LEFT, x, y, 0);
break;
default:
break;
}
}
}
In der Tat werden hier Ereignisse von Eingabegeräten in ein Format konvertiert, das Nuklear versteht. Eigentlich ist das wahrscheinlich alles.
Starten Sie auf einem anderen Board
Nachdem wir recht gute Ergebnisse erhalten hatten, beschlossen wir, sie auf einem anderen Board zu reproduzieren. Wir hatten ein anderes ähnliches Board - STM32F769I-DISCO. Es gibt denselben LTDC-Controller, aber einen anderen Bildschirm mit einer Auflösung von 800 x 480. Nach dem Start bekam es 25 FPS. Das heißt, ein spürbarer Leistungsabfall. Dies lässt sich leicht durch die Größe des Framebuffers erklären - er ist fast dreimal größer. Das Hauptproblem stellte sich jedoch als anders heraus: Das Bild war sehr verzerrt, es gab kein statisches Bild zu dem Zeitpunkt, an dem sich das Widget an einer Stelle befinden sollte.
Der Grund war nicht klar, deshalb haben wir uns Standardbeispiele von STM32Cube angesehen. Es gab ein Beispiel mit doppelter Pufferung für diese bestimmte Karte. In diesem Beispiel bewegen die Entwickler im Gegensatz zur Methode mit geänderter Transparenz einfach den Zeiger auf den Framebuffer des VBLANK-Interrupts. Wir haben diese Methode bereits früher für das erste Board ausprobiert, aber sie hat nicht funktioniert. Mit dieser Methode für STM32F769I-DISCO haben wir jedoch einen ziemlich reibungslosen Bildwechsel von 25 FPS erzielt.
Erfreut testeten wir diese Methode erneut (mit neu angeordneten Zeigern) auf der ersten Karte, aber sie funktionierte bei hohen FPS immer noch nicht. Infolgedessen funktioniert die Methode mit Layer-Transparenzen (60 FPS) auf einer Karte und die Methode mit neu angeordneten Zeigern (25 FPS) auf der anderen. Nachdem wir die Situation besprochen hatten, beschlossen wir, die Vereinigung auf eine eingehendere Untersuchung des Grafikstapels zu verschieben.
Ergebnis
Fassen wir also zusammen. Das gezeigte Beispiel stellt ein einfaches, aber allgemeines GUI-Muster für Mikrocontroller dar - einige Tasten, eine Lautstärkeregelung oder etwas anderes. Dem Beispiel fehlt jede mit Ereignissen verbundene Logik, da der Schwerpunkt auf den Grafiken lag. In Bezug auf die Leistung haben wir einen ziemlich anständigen FPS-Wert erhalten.
Die akkumulierten Nuancen zur Leistungsoptimierung lassen den Schluss zu, dass Grafiken in modernen Mikrocontrollern immer komplizierter werden. Genau wie auf großen Plattformen müssen Sie jetzt den Prozessor-Cache überwachen, etwas im externen Speicher und etwas im schnelleren Speicher ablegen, DMA verwenden, DMA2D verwenden, VBLANK überwachen und so weiter. Alles sah aus wie große Plattformen, und vielleicht habe ich deshalb schon mehrmals auf X Server und Wayland Bezug genommen.
Vielleicht ist einer der am wenigsten optimierten Teile das Rendering selbst. Wir zeichnen die gesamte Szene von Grund auf neu. Ich kann nicht sagen, wie es in anderen Bibliotheken für Mikrocontroller gemacht wird, vielleicht ist diese Phase irgendwo in die Bibliothek selbst eingebaut. Aber basierend auf den Ergebnissen der Arbeit mit Nuklear scheint es, dass an dieser Stelle ein Analogon von X Server oder Wayland benötigt wird, natürlich leichter, was uns wieder zu der Idee führt, dass kleine Systeme dem Weg großer folgen.
UPD1
Infolgedessen wurde die Methode zum Ändern der Transparenz nicht benötigt. Auf beiden Karten funktionierte ein gemeinsamer Code - mit dem Austausch der Pufferadresse durch V-Sync. Darüber hinaus ist die Methode mit Folien auch korrekt, es ist einfach nicht notwendig.
UPD2
Ich möchte mich ganz herzlich bei allen bedanken, die eine dreifache Pufferung vorgeschlagen haben. Wir sind noch nicht dazu gekommen. Aber jetzt können Sie sehen, dass dies eine klassische Methode ist (insbesondere für FPS mit hohen Bildraten auf dem Display), die es uns unter anderem ermöglicht, Verzögerungen aufgrund des Wartens auf die V-Synchronisierung zu beseitigen (d. H. Wenn die Software dem Bild merklich voraus ist). Wir haben dies noch nicht erlebt, aber es ist nur eine Frage der Zeit. Und ein besonderes Dankeschön für die Diskussion über Triple Puffering möchte ich sagenbesitzeruf und belav!
Unsere Kontakte:
Github: https://github.com/embox/embox
Newsletter: embox-ru [at] googlegroups.com
Telegramm-Chat: t.me/embox_chat