pico-watch

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



Hardware

- Rundes 1,28-Zoll-IPS-LCD-Display
- RP Pico 2040 (inclusive)
- 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. Die laden Sie sich direkt von hier herunter
(https://circuitpython.org/board/waveshare_rp2040_lcd_1_28/) und installieren sie auf dem Board.
Weiter 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 Pico 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 68 ins Thonny. Sie können auch schon mal 'start' drücken. Es erscheint das Zifferblatt.

  
  

1   # SPDX-FileCopyrightText : 2023 Detlef Gebhardt, written for CircuitPython
2   # SPDX-FileCopyrightText : Copyright (c) 2023 Detlef Gebhardt
3   # SPDX-Filename          : pico-watch
4   # SPDX-License-Identifier: GEBMEDIA
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  # Variable definieren
20  text = "pico-watch\n    by\n gebmedia"
21
22  # Bitmap-Dateien für Hintergrund und Zeiger
23  zifferblatt = "/images/zifferblatt.bmp"
24  second_zeiger = "/images/second.bmp"
25  minute_zeiger = "/images/minute.bmp"
26  hour_zeiger = "/images/hour.bmp"
27  #
28  # Display initialisieren
29  #
30  # Ressourcen freigeben für das Display
31  displayio.release_displays()
32
33  # displayio SPI-Bus und GC9A01 Display
34  spi = busio.SPI(clock=board.LCD_CLK, MOSI=board.LCD_DIN)
35  display_bus = displayio.FourWire(spi, command=board.LCD_DC, chip_select=board.LCD_CS, reset=board.LCD_RST)
36  display = gc9a01.GC9A01(display_bus, width=240, height=240, rotation=90, backlight_pin=board.LCD_BL)
37
38  main = displayio.Group()
39  display.show(main)
40
41  # Ziffernblatt als Hintergrund
42  bg_bitmap,bg_pal = adafruit_imageload.load(zifferblatt)
43  bg_tile_grid = displayio.TileGrid(bg_bitmap, pixel_shader=bg_pal)
44  main.append(bg_tile_grid)
45
46  # Text
47  text_area = label.Label(terminalio.FONT, text=text, line_spacing=0.9, color=0x000000, anchor_point=(0.5,0.5), anchored_position=(0,0))
48  text_group = displayio.Group(scale=1, x=120, y=80)
49  text_group.append(text_area)
50  main.append(text_group)
51
52  # Stundenzeiger 30x140
53  bitmap_pointer_hour, palette_pointer = adafruit_imageload.load(hour_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette)
54  palette_pointer.make_transparent(0)
55  # Blank bitmap vom Stundenzeiger
56  bitmap_pointer_blank_hour = displayio.Bitmap(bitmap_pointer_hour.width, bitmap_pointer_hour.height, 1)
57
58  # Minutenzeiger 30x140
59  bitmap_pointer_min, palette_pointer = adafruit_imageload.load(minute_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette)
60  palette_pointer.make_transparent(0)
61  # Blank bitmap vom Minutenzeiger
62  bitmap_pointer_blank_min = displayio.Bitmap(bitmap_pointer_min.width, bitmap_pointer_min.height, 1)
63
64  # Sekundenzeiger 30x140 pointer
65  bitmap_pointer_sec, palette_pointer = adafruit_imageload.load(second_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette)
66  palette_pointer.make_transparent(0)
67  # Blank bitmap vom Sekundenzeiger
68  bitmap_pointer_blank_sec = displayio.Bitmap(bitmap_pointer_sec.width, bitmap_pointer_sec.height, 1)
69
  

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 (70 bis 88) im Thonny ein.

  
  

70  # Transparentes Overlay für 'rotozoom'
71  # Zeiger für die Drehung
72  bitmap_scribble_hour = displayio.Bitmap(display.width, display.height, len(palette_pointer))
73  tile_grid = displayio.TileGrid(bitmap_scribble_hour, pixel_shader=palette_pointer)
74  main.append(tile_grid)
75  bitmap_scribble_min = displayio.Bitmap(display.width, display.height, len(palette_pointer))
76  tile_grid = displayio.TileGrid(bitmap_scribble_min, pixel_shader=palette_pointer)
77  main.append(tile_grid)
78  bitmap_scribble_sec = displayio.Bitmap(display.width, display.height, len(palette_pointer))
79  tile_grid = displayio.TileGrid(bitmap_scribble_sec, pixel_shader=palette_pointer)
80  main.append(tile_grid)
81  circle1 = Circle(120, 120, 10, fill=0xff0000, outline=None)
82  main.append(circle1)
83  circle2 = Circle(120, 120, 5, fill=0x000000, outline=0x0)
84  main.append(circle2)
85
86  # Zeichnen initialisieren
87  display.refresh()
88
  

Es folgt ein wichtiger 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 94 bis 98 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 wieder von Null los. Ist ein Akku angeschlossen, läuft das Display sogar ohne Unterbrechung weiter.

Hier kommt auch die Rolle der Datei 'boot.py' zu Tragen. Der Pico sucht beim Bootvorgang nach dieser Datei. Ist 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.

  
  

89  current_time = time.localtime()
90  hour = current_time.tm_hour
91  minute = current_time.tm_min
92  second = current_time.tm_sec
93
94  if current_time.tm_year > 2020:
95      with open("/time.txt", "w") as f:
96          f.write(str(hour)+"\n")
97          f.write(str(minute)+"\n")
98      f.close()
99  if current_time.tm_year == 2020:
100     with open("time.txt", "r") as f:
101         hour = int(f.readline())
102         minute = int(f.readline())
103     f.close()
104     second = current_time.tm_sec
105 if hour > 12:
106    hour = hour -12
107
  

In der 'while'-Schleife ab Zeile 108 wird jetzt bei jedem Durchlauf die aktuelle Sekunde bestimmt. Die Stunde und die Minute wird aus dem Bereich vor der Schleife verwendet. In den Zeilen 111, 113 und 115 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 112, 114 und 116 stellen die Bitmaps der Zeiger in entsprechend gedrehter Position dar. Ab Zeile 119 werden immer bei Sekunde 59 die Werte für 'Minute' um eins erhöht, bei Minute = 60 die Stunde erhöht u.s.w. Jede Minute werden die Werte für 'Stunde' und 'Minute' in der Datei 'time.txt' aktualisiert. Dabei hat die Zeit des Speichervorgangs keinen Einfluss auf die Ganggenauigkeit, denn die Sekunden werden ja vom MC weitergezählt. Die Pause time.sleep(0.7) in Zeile 130 hat die Aufgabe zu verhindern, dass der Speichervorgang schon abgeschlossen und die Sekunde noch bei 59 ist. Dann würde die Minute auch gleich mehrfach hochgezählt. Auch diese Pause wirkt sich nicht auf die Ganggenauigkeit aus. Da die Sekunden unabhängig weiterzählen, springt der Sekundenzeiger höchstens mal zwei Positionen weiter.

  
  

108 while True:
109     current_time = time.localtime()
110     second = current_time.tm_sec
111     alpha_rad_sec = math.pi/30 * second
112     bitmaptools.rotozoom( bitmap_scribble_sec, bitmap_pointer_sec, angle = alpha_rad_sec, px=15,py=107)
113     alpha_rad_hour = math.pi/6 * hour + math.pi/180 * minute/2
114     bitmaptools.rotozoom( bitmap_scribble_hour, bitmap_pointer_hour, angle = alpha_rad_hour, px=15,py=107)
115     alpha_rad_min = math.pi/30 * minute
116     bitmaptools.rotozoom( bitmap_scribble_min, bitmap_pointer_min, angle = alpha_rad_min, px=15,py=105)
117     if second == 59:
118         minute += 1
119         if minute == 60:
120             minute = 0
121             hour += 1
122             if hour == 24:
123                 hour = 0
124         with open("/time.txt", "w") as f:
125             f.write(str(hour)+"\n")
126             f.write(str(minute)+"\n")
127         f.close()
128         display.refresh()
129         #bitmaptools.rotozoom( bitmap_scribble_sec, bitmap_pointer_blank_sec, angle = alpha_rad_sec, px=15,py=105)
130         time.sleep(0.7)
131     gc.collect()
132     print(gc.mem_free())
  

Ihnen ist sicher der Befehl gc.collect() in Zeile 131 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.

Über einen Kommentar oder ein Lob hier an dieser Stelle würde ich mich sehr freuen.



Viel Spass und Erfolg beim Ausprobieren.