pico-watch

auf rundem 1.28-Zoll-IPS-LCD-Display 240 x 240 Pixel
- Waveshare 22668
Anleitung für die Firmware CircuitPython 8.2.4

erstellt: 13.4.2023,       letztes update: 06.08.2025


Hardware

- Rundes 1,28-Zoll-IPS-LCD-Display (WS 22668)
- USB-A zu USB-C Kabel


Mit dieser Anleitung schlagen wir ein neues Kapitel bei der Programmierung des runden Pico-LCD-Displays für alle Hobbyprogrammierer auf. Nachdem bisher überwiegend vielsagende Bilder von Anwendungen auf dem runden Pico-LCD-Display im Netz zu sehen waren, die von zugegebenermaßen Experten in C programmiert wurden, wird jetzt ein Zugang für alle Interessierten in CircuitPython geschaffen. Eine oft gewünschte Anwendung ist dabei z.B. ein Display mit einer Analoguhr. In der folgenden Anleitung geht es genau darum. Dabei stehen nicht das schönste Zifferblatt oder attraktive Zeiger, sondern die Funktionen im Mittelpunkt. Diese werden genau erklärt, so dass jeder seine eigene Uhr designen kann.

Los gehts

Als erstes wird die Firmeware 'CircuitPython' für den Pico benötigt. Da die bei der Weiterentwicklung stets etwas mehr Speicherplatz beansprucht, kommt es bei dem sehr begrenzten Angebot des RP2040 beim Start des Programms zum Memory-Error und damit Programmabruch. Deshalb verwenden Sie die zum Zeitpunkt der Erstellung aktuelle Firmware CircuitPython 8.2.4 und die dazu gehörenden Bibliotheken. Die Firmeware 8.2.4 können Sie sich hier herunterladen, entpacken und auf dem Board installieren.

Weiterhin brauchen Sie im 'lib-Ordner' die Bibliotheken adafruit_imageload, adafruit_display_shapes, adafruit_display_text sowie den Displaytreiber gc9a01.py. Die können Sie bei mir als zip-Datei herunterladen, entpacken und in den 'lib_Ordner' kopieren. In der zip_Datei ist auch ein Ordner 'images'. Darin sind die Bitmaps mit dem Zifferblatt und den Zeigern. Ziehen Sie diesen Ordner ins Stammverzeichnis von 'CIRCUITPY'. Ebenso die Datei 'boot.py', welche beim Speichern der Uhrzeit auf dem Picodisplay eine Rolle spielt, auf die ich später noch eingehe. Jetzt gehts richtig los.

Im unteren Kasten sehen Sie, wie das Display initialisiert wird. Kopieren Sie die Zeilen 1 bis 58 ins Thonny. Sie können auch schon mal 'start' drücken. Es erscheint das Zifferblatt. Speichern Sie die Datei als 'code.py'. Die Rückrage zum Überschreiben der vorhanden 'code.py' beantworten Sie mit 'ja'.

  
  
1   # SPDX-FileCopyrightText : 2023 Detlef Gebhardt, written for CircuitPython 8.2.4
2   # SPDX-FileCopyrightText : Copyright (c) 2023 Detlef Gebhardt
3   # SPDX-Filename          : pico-watch
4   # SPDX-License-Identifier: update 06.08.2025
5   import time
6   import gc
7   import board
8   import busio
9   import displayio
10  import digitalio
11  import terminalio
12  import bitmaptools
13  import math
14  import adafruit_imageload
15  from adafruit_display_text import label
16  from adafruit_display_shapes.circle import Circle
17  import gc9a01
18
19  # Bitmap-Dateien fuer Hintergrund und Zeiger
20  zifferblatt = "/images/zifferblatt.bmp"
21  second_zeiger = "/images/second.bmp"
22  minute_zeiger = "/images/minute.bmp"
23  hour_zeiger = "/images/hour.bmp"
24  #
25  # Display initialisieren
26  #
27  # Ressourcen freigeben fuer das Display
28  displayio.release_displays()
29  # displayio SPI-Bus und GC9A01 Display
30  spi = busio.SPI(clock=board.LCD_CLK, MOSI=board.LCD_DIN)
31  display_bus = displayio.FourWire(spi, command=board.LCD_DC, chip_select=board.LCD_CS, reset=board.LCD_RST)
32  display = gc9a01.GC9A01(display_bus, width=240, height=240, rotation=90, backlight_pin=board.LCD_BL)
33  main = displayio.Group()
34  display.show(main)
35
36  # Ziffernblatt als Hintergrund
37  bg_bitmap,bg_pal = adafruit_imageload.load(zifferblatt)
38  bg_tile_grid = displayio.TileGrid(bg_bitmap, pixel_shader=bg_pal)
39  main.append(bg_tile_grid)
40
41  # Stundenzeiger 30x140
42  bitmap_pointer_hour, palette_pointer = adafruit_imageload.load(hour_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette)
43  palette_pointer.make_transparent(0)
44  # Blank bitmap vom Stundenzeiger
45  bitmap_pointer_blank_hour = displayio.Bitmap(bitmap_pointer_hour.width, bitmap_pointer_hour.height, 1)
46
47  # Minutenzeiger 30x140
48  bitmap_pointer_min, palette_pointer = adafruit_imageload.load(minute_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette)
49  palette_pointer.make_transparent(0)
50  # Blank bitmap vom Minutenzeiger
51  bitmap_pointer_blank_min = displayio.Bitmap(bitmap_pointer_min.width, bitmap_pointer_min.height, 1)
52
53  # Sekundenzeiger 30x140 pointer
54  bitmap_pointer_sec, palette_pointer = adafruit_imageload.load(second_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette)
55  palette_pointer.make_transparent(0)
56  # Blank bitmap vom Sekundenzeiger
57  bitmap_pointer_blank_sec = displayio.Bitmap(bitmap_pointer_sec.width, bitmap_pointer_sec.height, 1)
58
  

Im nächsten Schritt werden die transparenten Overlays für die zu drehenden Zeiger angelegt. Schließlich werden zwei kleine Kreise als zentraler Mittelpunkt definiert. Fügen Sie auch diese Zeilen (59 bis 74) im Thonny ein.

  
  
59  # Transparentes Overlay fuer 'rotozoom'
60  # Zeiger fuer die Drehung
61  bitmap_scribble_hour = displayio.Bitmap(display.width, display.height, len(palette_pointer))
62  tile_grid = displayio.TileGrid(bitmap_scribble_hour, pixel_shader=palette_pointer)
63  main.append(tile_grid)
64  bitmap_scribble_min = displayio.Bitmap(display.width, display.height, len(palette_pointer))
65  tile_grid = displayio.TileGrid(bitmap_scribble_min, pixel_shader=palette_pointer)
66  main.append(tile_grid)
67  bitmap_scribble_sec = displayio.Bitmap(display.width, display.height, len(palette_pointer))
68  tile_grid = displayio.TileGrid(bitmap_scribble_sec, pixel_shader=palette_pointer)
69  main.append(tile_grid)
70  circle1 = Circle(120, 120, 10, fill=0xff0000, outline=None)
71  main.append(circle1)
72  circle2 = Circle(120, 120, 5, fill=0x3D55CD, outline=0x0)
73  main.append(circle2)
74
  

Es folgt ein Teil, der für die korrekte Uhrzeit wichtig ist. Wenn das Display am Rechner angeschlossen ist, holt es sich die Zeit von 'time.localtime()' und speichert sie in den Zeilen 80 bis 84 in die Datei 'time.txt'. Ist das Display hingegen nicht am Rechner, dann beginnt 'localtime()' am 1.1.2020 um 00:00 Uhr die Sekunden zu zählen. Für diesen Fall lesen wir die letzte bekannte Stunde und Minute aus 'time.txt'. Zieht man z.B. das Display vom Rechner ab und schließt es sofort an eine andere Stromversorgung (z.B. Powerbank) an, "verliert" man zwar einige Sekunden, denn der MC zählt die Sekunde wieder bei Null beginnend. Ist ein Akku angeschlossen, läuft das Display ohne Unterbrechung weiter. Die manuelle Einstellung der jeweiligen Uhrzeit ohne angeschlossenen PC wird in einem späteren Beitrag gezeigt.

Hier kommt jetzt die Rolle der Datei 'boot.py' zum Tragen. Das Picodisplay sucht beim Bootvorgang nach dieser Datei. Ist dort der Befehl storage.remount("/", False) vorhanden, dann wird der interne Speicher zum Beschreiben aktiviert. Ansonsten ist das Pico-Laufwerk schreibgeschützt. So wird im Normalfall verhindert, dass ein anderer Rechner auf den Pico schreibt. Jede Änderung in der Datei 'boot.py', wird erst nach einem erneuten Bootvorgang wirksam (also z.B. Strom unterbrechen und wieder anschließen). Wenn Sie das nach dem Übertragen der Bibliotheksdateien (s.o.) nocht nicht getan haben, sollten Sie das jetzt tun, damit später nicht der Fehler: "Laufwerk schreibgeschützt" kommt.

  
  
75  current_time = time.localtime()
76  hour = current_time.tm_hour
77  minute = current_time.tm_min
78  second = current_time.tm_sec
79
80  if current_time.tm_year > 2020:
81      with open("/time.txt", "w") as f:
82          f.write(str(hour)+"\n")
83          f.write(str(minute)+"\n")
84      f.close()
85  if current_time.tm_year == 2020:
86      with open("time.txt", "r") as f:
87          hour = int(f.readline())
88          minute = int(f.readline())
89      f.close()
90      second = current_time.tm_sec
91
  

In der 'while'-Schleife ab Zeile 92 wird jetzt bei jedem Durchlauf die aktuelle Sekunde bestimmt. Die Stunde und die Minute wird aus dem Bereich vor der Schleife bzw. den Zeilen 103 u. 104 verwendet. In den Zeilen 95 bis 100 werden die Winkel der Zeiger in Abhängigkeit von den Sekunden, Minuten und Stunden (im Bogenmaß) berechnet. Dazu finden Sie Erklärungen in der Anleitung 6 (hier). Die Zeilen 96, 98 und 100 stellen die Bitmaps der Zeiger in entsprechend gedrehter Position dar. Ab Zeile 101 werden immer bei Sekunde 0 die Werte für 'Minute' und 'Stunde' in der Datei 'time.txt' aktualisiert (Zeilen 106 bis 109). Dabei hat die Zeit des Speichervorgangs keinen Einfluss auf die Ganggenauigkeit, denn die Sekunden werden ja vom MC unabhängig weitergezählt.

  
  
92  while True:
93      current_time = time.localtime()
94      second = current_time.tm_sec
95      alpha_rad_sec = math.pi/30 * second
96      bitmaptools.rotozoom( bitmap_scribble_sec, bitmap_pointer_sec, angle = alpha_rad_sec, px=15,py=107)
97      alpha_rad_hour = math.pi/6 * hour + math.pi/180 * minute/2
98      bitmaptools.rotozoom( bitmap_scribble_hour, bitmap_pointer_hour, angle = alpha_rad_hour, px=15,py=107)
99      alpha_rad_min = math.pi/30 * minute
100     bitmaptools.rotozoom( bitmap_scribble_min, bitmap_pointer_min, angle = alpha_rad_min, px=15,py=105)
101     if second == 0:
102         current_time = time.localtime()
103         hour = current_time.tm_hour
104         minute = current_time.tm_min
105         second = current_time.tm_sec
106         with open("/time.txt", "w") as f:
107             f.write(str(hour)+"\n")
108             f.write(str(minute)+"\n")
109         f.close()
110         display.refresh()
111
112     gc.collect()
113     #print(gc.mem_free())
  

Ihnen ist sicher der Befehl gc.collect() in Zeile 112 aufgefallen. Bei jedem Schleifendurchlauf sorgt er dafür, nicht mehr benötigten RAM freizugeben. Wenn Sie diesen Befehl auskommentieren und den angezeigten freien Speicher beobachten, werden Sie sehen, dass es schnell knapp werden kann. Deshalb achten Sie auch darauf, dass Sie bei eigenen Hintergründen und Zeigern eher etwas knauserig mit der Farbtiefe der Bitmaps sind. Lieber nur 16 Farben statt 256.


Viel Spass und Erfolg beim Ausprobieren.