Czas systemowy
Aby uzyskać dostęp do różnych funkcji związanych z odmierzaniem czasu, najpierw musimy zaimportować moduł time.
W Pythonie istnieją dwie funkcje zwracające aktualny czas: gmtime oraz localtime. Różnica między nimi polega na tym, że gmtime zwraca czas uniwersalny UTC, który trzeba przekształcić na zapis odpowiadający interesującej nas strefie czasowej. Funkcja localtime wykonuje to zadanie, uwzględnia także czas letni lub zimowy. Jednak w MicroPythonie nie zaimplementowano obsługi stref czasowych ani czasu letniego, więc obie funkcje zwracają dokładnie ten sam czas.
Kiedy w konsoli wywołamy jedną z tych funkcji, otrzymamy następujący rezultat:
>>> import time
>>> time.localtime()
(2025, 2, 23, 8, 43, 20, 6, 54)
Obie funkcje zwracają czas w postaci time tuple, czyli krotki czasu. Krotka jest czymś w rodzaju tablicy z nawiasami okrągłymi, gdzie wszystkie elementy są tylko do odczytu. Można odczytywać pojedyncze elementy za pomocą operatora [i], gdzie i to numer żądanego elementu, a liczenie zaczynamy od zera. Krotka czasu składa się z ośmiu elementów. Są to kolejno: rok, miesiąc, dzień, godzina, minuta, sekunda, dzień tygodnia (od 0 do 6, a pierwszym dniem tygodnia jest poniedziałek), numer dnia w roku.
Pewnie się zastanawiasz, skąd ESP32-S3 wie, jaka jest aktualna godzina, skoro jeszcze nigdy jej nie ustawiłeś? Program Thonny przesyła aktualną godzinę zaraz po nawiązaniu połączenia z mikrokontrolerem. Tę funkcjonalność można wyłączyć, wybierając menu Narzędzia, Opcje, Interpreter, a następnie klikamy Synchronizuj rzeczywisty zegar urządzenia.
W mikrokontrolerach z rodziny ESP32, podobnie jak w wielu innych, znajduje się wbudowany zegar czasu rzeczywistego (real time clock, RTC). Ten układ odmierza czas niezależnie od trybu pracy i może działać bez przerwy, nawet w trybie najgłębszego uśpienia. Wystarczy tylko podać mu aktualną godzinę.
Warto wiedzieć, że układ RTC w ESP32 ma możliwość wybudzania procesora z uśpienia po upływie zadanego czasu. Ponadto RTC ma wbudowaną pamięć RAM o pojemności 16 kB, a nawet własny procesor! ESP32-S3 wyposażony jest w dwa dodatkowe procesory w układzie RTC, zwane procesorami ULP (ultra low power). Jeden z nich to popularny RISC-V, a drugi to ULP-FSM, który jest autorskim dziełem firmy Espressif i można na niego napisać program tylko w asemblerze. Są to tematy na osobny odcinek, więc wróćmy do odmierzania czasu.
Aby uzyskać dostęp do zegara RTC, musimy utworzyć instancję klasy, która nim zarządza. Tę klasę znajdziemy w module machine, który należy zaimportować. Następnie w zmiennej new_time tworzymy krotkę czasu, podając do niej aktualną datę i godzinę.
Uwaga!!! Krotka czasu klasy RTC jest inna niż krotka czasu modułu time! Jest to dość irytujący brak konsekwencji ze strony twórców MicroPythona, przez co można łatwo popełnić błąd. Należy podać kolejno: rok, miesiąc, dzień, numer dnia tygodnia (najlepiej podać 0 i dzień tygodnia zostanie uzupełniony automatycznie), godzinę, minutę, sekundę, subsekundy (czyli najlepiej 0).
W poniższym przykładzie ustawiamy datę na 24 grudnia 2000 i godzinę na 12:34:56. Następnie tworzymy instancję klasy RTC i zapisujemy ją do zmiennej rtc. Kolejnym krokiem jest wywołanie metody datetime i podanie do niej krotki czasu.
>>> import machine
>>> new_time = (2000, 12, 24, 0, 12, 34, 56, 0)
>>> rtc = machine.RTC()
>>> rtc.datetime(new_time)
Można też krócej.
>>> import machine
>>> machine.RTC().datetime((2000, 12, 24, 0, 12, 34, 56, 0))
Pamiętaj, że krotka zawsze objęta jest nawiasami okrągłymi, a jeżeli jest ona jedynym argumentem funkcji, wówczas w zapisie stosujemy podwójne nawiasy okrągłe.
Jeżeli wywołamy metodę datetime bez argumentu, to zwróci ona aktualny czas, ale w formacie stosowanym w klasie RTC. Dlatego należy przyzwyczaić się do funkcji localtime, ponieważ jest ona zgodna z „normalnym” Pythonem używanym na komputerach PC.
>>> time.localtime()
(2000, 12, 24, 12, 34, 56, 6, 359)
>>> machine.RTC().datetime()
(2000, 12, 24, 6, 12, 34, 56, 287)
Zegar RTC typu DS1307
W dalszej części poznamy, jak w MicroPythonie obsługuje się interfejs I²C. Do ćwiczeń użyjemy popularnego układu zegarowego DS1307. Oczywiście można użyć dowolnego modułu uniwersalnego zawierającego ten układ, a zapewne jest możliwe, że już masz tego typu płytkę pod ręką i jesteś już zaznajomiony z tym układem. W takiej sytuacji przeskocz do kolejnego rozdziału.
Głównym zadaniem układu DS1307 jest wyznaczanie aktualnej godziny, niezależnie od tego, co dzieje się z resztą urządzenia. Zegar RTC ma swój własny kwarc o częstotliwości 32 768 Hz i nie wymaga dodatkowych kondensatorów. Ma także osobny pin do zasilania z baterii litowej o napięciu 3 V lub superkondensatora. Omawiany model ma już swoje lata i z tego powodu wymaga zasilania napięciem 5 V, ale nic nie stoi na przeszkodzie, by współpracował z układami zasilanymi napięciem 3,3 V. W takiej sytuacji rezystory pull-up na liniach SDA i SCL muszą być podłączone do napięcia 3,3 V. Z pinu SQW można pobrać sygnał o określonej częstotliwości, a najczęściej korzysta się z niego podczas testów, by sprawdzić częstotliwość kwarcu. Typowy sposób podłączenia zegara DS1307 przedstawia pokazuje schemat widoczny na rysunku 1.
DS1307 na magistrali I²C pełni funkcję układu slave. To znaczy, że sam nigdy nie inicjuje połączenia, a jedynie oczekuje, aż master rozpocznie transmisję. Z punktu widzenia mastera zegar DS1307 jest pamięcią o pojemności 64 bajtów. Każdy z tych bajtów ma swój unikalny, 8-bitowy adres, a pierwsze osiem pozycji ma specjalne znaczenie – zobacz rysunek 2. Bajty o adresach 0x00...0x06 określają aktualną datę i godzinę. Zwróć uwagę, że te liczby są zapisywane w kodzie BCD, czyli po cztery bity na każdą cyfrę. Bajt pod adresem 0x07 konfiguruje pracę pinu SQW. Jeżeli bit 7 bajtu pod adresem 0x00 jest w stanie wysokim, to znaczy, że czas nie został jeszcze prawidłowo ustawiony.
Zobaczmy, jak wygląda ustawianie daty i godziny – dla przykładu ustawimy godzinę 12:34:56 w dniu 24.12.2025. Zapis sygnałów na liniach SCL i SDA pokazuje rysunek 3. Pierwszym bajtem przesyłanym przez mastera jest adres układu slave, który ma odebrać następującą transmisję. W tym przypadku DS1307 jest to zawsze 0x68 (zaznaczone na niebiesko). Kolejny bajt określa adres w pamięci, który ma zostać zapisany jako pierwszy. W naszym przykładzie jest to 0x00, a zgodnie z rysunkiem 2, pod tym adresem należy zapisać sekundy. Dalej kolejne bajty oznaczają wartości liczbowe: sekundy, minuty, godzinę, dzień tygodnia, dzień, miesiąc i rok zapisane w kodzie BCD.
Odczyt danych wykonuje się w dwóch etapach. Zobacz rysunek 4. Pierwszym etapem jest wywołanie adresu układu DS1307 w trybie zapisu. Po tym master przesyła adres bajtu, jaki zamierza odczytać jako pierwszy. Chcemy odczytać wszystko, łącznie z sekundami i dlatego przesyłamy 0x00. Gdybyśmy chcieli odczytać godzinę i minuty, pomijając sekundy, moglibyśmy tutaj przesłać 0x01. Po przesłaniu tego bajtu master kończy transmisję.
Drugi etap polega na ponownym wywołaniu adresu 0x68 w trybie odczytu. Master może odczytać tyle bajtów ile chce, a slave przesyła bajty, rozpoczynając od tego, jaki został wskazany w pierwszym etapie.
Znamy już podstawy interfejsu I²C oraz sposób obsługi zegara DS1307, więc poznajmy teraz, jakie „magiczne zaklęcia” należy wpisać, aby wykonać te operacje w MicroPythonie.
Interfejs I²C
MicroPython oferuje dwa rodzaje interfejsów. Są to Soft I²C oraz Hard I²C. Ten pierwszy korzysta z całkowicie programowej implementacji I²C i może być użyty na dowolnym mikrokontrolerze i dowolnych pinach. Interfejs Hard I²C korzysta ze sprzętowego peryferium. W wielu mikrokontrolerach linie SDA i SCL muszą być połączone do ściśle określonych pinów, aby obsłużyć sprzętowy interfejs I²C, ale mikrokontrolerów z rodziny ESP32 to nie dotyczy. Te układy są wyposażone w matrycę połączeniową, która pozwala przekierować wyjścia SDA i SCL z transmitera na dowolne piny (poza kilkoma wyjątkami). Daje to bardzo dużą elastyczność, przez co Soft I²C traci sens, więc nie będziemy go omawiać.
Aby uzyskać dostęp do I²C, musimy zaimportować klasę I²C z modułu machine. Następnie tworzymy instancję tej klasy, przekazując do konstruktora cztery argumenty:
- id – oznacza numer sprzętowego interfejsu I²C. W ESP32-S3 dostępne są dwa takie interfejsy, oznaczone numerami 0 i 1.
- scl – wyjście zegarowe, należy tu podać instancję klasy Pin.
- sda – wejście/wyjście danych, należy tu podać instancję klasy Pin.
- freq – częstotliwość zegara na magistrali I²C, wspólna dla wszystkich slave’ów.
Argument id jest argumentem pozycyjnym i musi występować jako pierwszy, a argumenty scl, sda i freq zawsze muszą być nazwane. Argument freq można pominąć i wtedy interfejs będzie pracował z częstotliwością 400 kHz. Zobaczmy na przykładzie, jak utworzyć instancję klasy I²C i zapisać ją w zmiennej i2c.
>>> from machine import Pin, I2C
>>> i2c = I2C(0, scl=Pin(1), sda=Pin(2), freq=100000)
Warto wiedzieć, że tak utworzony obiekt można wydrukować na konsoli, aby zobaczyć jego konfigurację.
>>> print(i2c)
I2C(0, scl=1, sda=2, freq=100000)
Każde urządzenie na magistrali I²C musi mieć jakiś adres i odpowiedzieć na jego wywołanie. W ten sposób możemy przeskanować wszystkie adresy od 0x00 do 0x7F (czyli od 0 do 127 w zapisie dziesiętnym). Napiszmy prosty program, który wyświetli adresy wszystkich urządzeń dostępnych na szynie komunikacyjnej:
# Plik i2c_scan.py
from machine import Pin, I2C # 1
i2c = I2C(0, scl=Pin(1), sda=Pin(2), freq=100000) # 2
devices = i2c.scan() # 3
print("Znalezione urządzenia: ", end="")
for device in devices: # 4
print(f"{device:02X} ", end="") # 5
Linie 1 i 2 już znamy. W linii 3 wywołujemy metodę scan z klasy I²C. Jako rezultat zwraca ona listę, którą zapisujemy do zmiennej devices. Moglibyśmy tę listę wyświetlić od razu za pomocą funkcji print, lecz wtedy zobaczyliśmy na konsoli adresy zapisane w notacji dziesiętnej. Przyjęło się, by podawać adresy w notacji szesnastkowej. Aby je przekształcić, posłużymy się prostą pętlą for (linia 4), która odczytuje po kolei każdy element z listy devices i zapisuje go do zmiennej device. Wewnątrz pętli znajduje się tylko jedna instrukcja print (linia 5), wyświetlająca f-string, w którym zawarta jest zmienna device w nawiasach klamrowych. Zapis :02X oznacza, że zmienna ma zostać wyświetlona w notacji szesnastkowej i ma składać się z dwóch znaków z zerem wiodącym (jeśli trzeba).
Aby przesłać jakieś dane do urządzenia na magistrali I²C, możemy skorzystać z metody writeto. Pierwszym argumentem jest adres układu slave (w przykładzie: 0x68), który ma odebrać transmisję, a drugim są dane do wysłania. Dane koniecznie muszą być w postaci obiektu typu bytes lub bytearray. Najważniejsza różnica między nimi jest taka, że bytes jest tylko do odczytu, a bytearray można modyfikować.
Poniżej pokazano trzy przykłady wykonania takiej operacji. Przykład z pierwszej linijki jest optymalny pod względem zapotrzebowania na pamięć. W przykładach 2 i 3, oprócz stałych, można używać także jakichś zmiennych, pod warunkiem,że są to liczby całkowite z zakresu od 0 do 255.
Po otrzymaniu każdego bajtu urządzenie slave powinno potwierdzić odbiór poprzez wysłanie sygnału ACK do mastera. Funkcja writeto zwraca liczbę otrzymanych potwierdzeń ACK.
>>> i2c.writeto(0x68, b'\x11\x22\x33\x44') # 1
>>> i2c.writeto(0x68, bytes([0x11, 0x22, 0x33, 0x44])) # 2
>>> i2c.writeto(0x68, bytearray([0x11, 0x22, 0x33, 0x44])) # 3
Inną opcją jest funkcja writevto, która umożliwia wysłanie „wektora” danych. Poprzez słowo wektor należy rozumieć listę lub krotkę, składającą się z obiektów bytes lub bytearray.
>>> a = b'\x20'
>>> b = b'\xAA\xBB\xCC\xDD'
>>> c = b'\x12\x34\x56\x78'
>>> vector = (a, b, c)
>>> i2c.writevto(0x68, vector)
W celu odczytania danych poprzez interfejs I²C możemy posłużyć się funkcją readfrom, której pierwszym argumentem jest adres urządzenia slave, a drugim liczba bajtów do odczytania. Jako rezultat funkcja zwraca obiekt typu bytes z odczytanymi bajtami. Aby odczytać 8 bajtów z urządzenia o adresie 0x68, należy wydać następujące polecenie:
Drugą opcją jest funkcja readfrom_into, która zapisuje odczytane bajty do istniejącego już bufora typu byte array. Funkcja odczyta tyle bajtów, ile wynosi długość podanego bufora. Poniżej przykład odczytania 8 bajtów ze slave’a o adresie 0x86.
>>> buffer = bytearray(8)
>>> i2c.readfrom_into(0x68, buffer)
Jaki jest sens tych dwóch funkcji i dlaczego nie wystarczy jedna z nich? Funkcja readfrom za każdym razem tworzy nowy bufor w pamięci. Ten bufor często jest później usuwany, np. w wyniku wyjścia z funkcji, która go stworzyła. Powoduje to konieczność alokowania nowej pamięci przy każdym wywołaniu readfrom, co oczywiście zajmuje czas, w szczególności gdy bufor ma być duży, a pamięć jest mocno pofragmentowana. Funkcja readfrom_into operuje na istniejącym już buforze, zatem nie alokuje pamięci. W rezultacie ta operacja działa szybciej.
MicroPython ma jeszcze dodatkowe trzy funkcje, które ułatwiają pracę z pamięciami. Jak już pisaliśmy, układ DS1307 jest de facto pamięcią, zatem funkcje te będą znakomicie się nadawały do współpracy z tym układem. Pierwszą komendą, jaką poznamy, będzie writeto_mem, przyjmująca cztery argumenty. Pierwszy to, jak zawsze, adres urządzenia na magistrali I²C, drugi to adres pamięci tego urządzenia, pod którym zamierzamy rozpocząć zapis danych. Trzeci to dane do zapisu, zaś czwarty – liczba bitów adresu, przy czym najczęściej spotyka się adresowanie 8, 16, 24 lub 32-bitowe. Ten argument można pominąć i wtedy domyślną wartością jest 8 bitów. Poniżej przykład pokazujący, jak zapisać bajty o wartościach AB CD EF w pamięci, zaczynając od adresu 0x30, w urządzeniu o adresie 0x68. Dwie poniższe linijki są równoważne.
>>> i2c.writeto(0x68, b'\x30\xAB\xCD\xEF') # 1
>>> i2c.writeto_mem(0x68, 0x30, b'\xAB\xCD\xEF', addrsize=8) # 2
O ile w przypadku zapisu może jeszcze nie widać różnicy, o tyle w przypadku odczytu jest ona istotna. Wszak odczyt składa się najpierw z zapisania adresu do odczytu, a dopiero potem właściwego pobrania interesujących nas informacji. W poniższym przykładzie linia 2 daje taki sam efekt, jak linie 1A i 1B, ale wykonuje się szybciej.
>>> i2c.writeto(0x68, b'\x30') # 1A
>>> buffer = i2c.readfrom(0x68, 3) # 1B
>>> buffer = i2c.readfrom_mem(0x68, 0x30, 3) # 2
Funkcja readfrom_mem zwraca odczytany bufor, a readfrom_mem_into zapisuje odczytane dane do utworzonego wcześniej bufora, wskazanego w argumencie.
>>> buffer = bytearray(3)
>>> i2c.writeto(0x68, b'\x30') # 1A
>>> i2c.readfrom_into(0x68, buffer) # 1B
>>> i2c.readfrom_mem_into(0x68, 0x30, buffer, addrsize=8) # 2
Moduł do obsługi zegara DS1307
Skoro już wiemy, jak w MicroPythonie obsługuje się interfejs I²C, to możemy wrócić do DS1307 i napisać kilka prostych funkcji do odczytywania i ustawiania czasu. Zaprezentujemy jedną z możliwych metodologii pisania kodu w MicroPythonie. Zrobimy to bardzo prosto – bez tworzenia żadnych klas, wyjątków ani w ogóle sprawdzania, czy interfejs I²C odczytał jakieś dane. Taki kod jest prosty jak przysłowiowa budowa cepa, ale trzeba pamiętać, że także mało odporny na błędy i potencjalnie trudny w integracji z jakimś większym projektem. Przeanalizujmy kod zamieszczony na listingu 1.
import time
from machine import Pin, I2C, RTC
_DS1307_ADDRESS = const(0x68) # 1
i2c = I2C(0, scl=Pin(1), sda=Pin(2), freq=100000)
def dump(): # 2
buffer = i2c.readfrom_mem(_DS1307_ADDRESS, 0x00, 64) # 3
print(" 0 1 2 3 4 5 6 7 8 9 A B C D E F")
for i in range(64):
if i % 16 == 0:
print(f"{i:02X}: ", end = "")
print(f"{buffer[i]:02X}", end="\n" if i%16 == 15 else " ")
def read(): # 4
buffer = i2c.readfrom_mem(_DS1307_ADDRESS, 0x00, 7) # 5
if buffer[0] & 0b10000000: # 6
print("Clock not set")
return None
def bcd2bin(value): # 7
tens = (value & 0xF0) >> 4
ones = (value & 0x0F)
return tens * 10 + ones
s = bcd2bin(buffer[0]) # Sekundy # 8
m = bcd2bin(buffer[1]) # Minuty
h = bcd2bin(buffer[2]) # Godziny
w = buffer[3] – 1 # Dzień tygodnia
D = bcd2bin(buffer[4]) # Dzień
M = bcd2bin(buffer[5]) # Miesiąc
Y = bcd2bin(buffer[6]) + 2000 # Rok
print(f"{Y}.{M:02}.{D:02} {h:02}:{m:02}:{s:02}") # 9
return (Y, M, D, h, m, s, w, 0) # 10
def write(time_tuple): # 11
def bin2bcd(value): # 12
tens = value // 10
ones = value % 10
return tens << 4 | ones
buffer = bytes([ # 13
bin2bcd(time_tuple[5]), # Sekundy
bin2bcd(time_tuple[4]), # Minuty
bin2bcd(time_tuple[3]), # Godziny
time_tuple[6] + 1, # Dzień tygodnia (1..7)
bin2bcd(time_tuple[2]), # Dzień
bin2bcd(time_tuple[1]), # Miesiąc
bin2bcd(time_tuple[0] – 2000), # Rok (00..99)
])
i2c.writeto_mem(_DS1307_ADDRESS, 0x00, buffer) # 14
def copy_time_from_ds1307_to_system(): # 15
Y, M, D, h, m, s, _, _ = read() # 16
new_time_tuple = (Y, M, D, 0, h, m, s, 0) # 17
RTC().datetime(new_time_tuple) # 18
if __name__ == "__main__": # 19
dump()
# read()
new_time = time.localtime()
# new_time = (2030, 04, 27, 12, 05, 00, 0, 0)
# new_time = (2025, 12, 24, 12, 34, 56, 0, 0)
write(new_time)
read()
Listing 1. Kod pliku ds1307.py
Na początku, jak zawsze, importujemy potrzebne zależności. Są to moduł time oraz klasy Pin, I²C i RTC z modułu machine. W linii 1 tworzymy stałą, w której zapisujemy adres układu DS1307 na magistrali I²C. Stałe to coś, czego w zwykłym Pythonie nie ma, ale znalazły zastosowanie w MicroPythonie. Wszystko to, co utworzymy za pomocą const(), jest stałą i nie można jej zmieniać. Stałe w MicroPythonie można porównać do #define z C++. Przed uruchomieniem programu wartość stałej zostaje wklejona we wszystkie miejsca, w których ta stała występuje, a następnie program jest kompilowany. Dzięki temu kod zajmuje mniej pamięci RAM i wykonuje się szybciej.
W linii 2 rozpoczynamy funkcję dump. Jej zadaniem jest odczytać całą pamięć z DS1307 i wyświetlić ją na konsoli w postaci szesnastkowej. W ten sposób będziemy mogli podejrzeć wszystkie 64 bajty pamięci tego układu. Oto przykładowy rezultat wywołania opisywanej funkcji:
0 1 2 3 4 5 6 7 8 9 A B C D E F
00: 44 28 09 05 07 03 25 03 00 00 48 00 80 00 14 80
10: 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
20: 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
30: AB CD EF 01 44 55 66 77 88 99 AA BB CC DD EE FF
Funkcja składa się z dwóch części. Pierwszą jest (linia 3) użycie readfrom_mem, aby odczytać 64 bajty – począwszy od adresu 0x00 – i zapisać je do zmiennej buffer. Następnie mamy pętlę for, w której wyświetlamy bufor bajt po bajcie, a po wyświetleniu 16 bajtów przechodzimy do kolejnej linii. Dodatkowo, wyświetlamy także nagłówki, aby ułatwić sobie odczytywanie tych wszystkich bajtów.
Kolejna jest funkcja read (linia 4), której zadanie to odczytanie aktualnej daty i godziny z DS1307, a następnie zwrócenie tych danych w postaci krotki czasu, kompatybilnej z modułem time. Podobnie jak w dump, rozpoczynamy pracę od wywołania funkcji readfrom_mem, ale tutaj odczytujemy tylko pierwsze 7 bajtów, zaczynając od adresu 0x00 (linia 5).
Aby dowiedzieć się, czy zegar został prawidłowo ustawiony, najpierw musimy sprawdzić najstarszy bit zerowego bajtu bufora (linia 6). Jeżeli jest on w stanie wysokim, wówczas wyświetlamy komunikat o błędzie i zwracamy None. Można by dodać w tym miejscu także zgłoszenie jakiegoś wyjątku.
W linii 7 tworzymy funkcję wewnątrz funkcji. Jej nazwa bcd2bin zdradza, jaki jest jej cel. Dane odczytane z DS1307 są zapisane w kodzie BCD, czyli starsze 4 bity kodują cyfrę dziesiątek, a młodsze cztery bity odpowiedzialne są za cyfrę jednostek. Aby móc wykonywać jakiekolwiek operacje w MicroPythonie, musimy te dane przekształcić na format binarny.
W linii 8 i kilku następnych wywołujemy funkcję bcd2bin, aby przekształcić kolejne bajty z bufora. W przypadku dnia tygodnia musimy odjąć jedynkę, a do oznaczenia roku musimy dodać 2000. Na potrzeby testów możemy wyświetlić odczytany czas na konsoli (linia 9). Wynik zwracamy w linii 10, łącząc poszczególne składowe daty i czasu w krotkę.
Aby móc zapisać czas do DS1307, opracowano funkcję write, która rozpoczyna się w linii 11. Jako argument przyjmuje ona krotkę czasu zgodną z modułem time. Podobnie jak w funkcji read, musimy tutaj również zamieścić wewnętrzną funkcję do konwersji liczb binarnych na format BCD (linia 12).
Następnie w linii 13 tworzymy bufor typu bytes. Jest to tablica bajtów tylko do odczytu. Tworząc ją, zapisujemy do niej wszystkie składowe daty i godziny, uprzednio konwertując je z formatu binarnego na BCD. Ostatnią czynnością jest wywołanie funkcji writeto_mem, która zapisze tak utworzony bufor pod adresem 0x00 w pamięci układu DS1307.
Na końcu mamy funkcję copy_time_from_ds1307_to_system (linia 15), która – jak można się domyślić – odczytuje czas z DS1307 i zapisuje go w zegarze systemowym RTC w ESP32. Byłoby idealnie, gdyby moduł RTC korzystał z takiej samej krotki czasu jak wszystko inne, ale tak nie jest, więc musimy zrobić kilka przekształceń.
W linii 16 wywołujemy funkcję read, którą zdefiniowaliśmy wcześniej. Funkcja co prawda zwraca krotkę, ale możemy ją natychmiast rozbić na pojedyncze zmienne. Aby kod nie był za bardzo rozwlekły, poszczególne składowe daty i godziny oznaczyłem pojedynczymi literami. Są to po kolei: rok, miesiąc, dzień, godzina, minuta, sekunda. Znak _ oznacza, że odpowiadająca mu składowa krotki jest niepotrzebna. Tworzymy nową krotkę, zmieniając kolejność składowych i wpisując zera w miejsca niepotrzebne, po czym zapisujemy ją do zmiennej new_time_tuple (linia 17). Pozostaje już tylko wywołać funkcję datetime z klasy RTC, do której przekazujemy nową krotkę czasu (linia 18).
W dalszej części kodu (linia 19) widzimy kilka przykładów korzystania z funkcji opracowanych w niniejszym odcinku kursu, gdzie odczytujemy czas, ustawiamy go i zaglądamy do pamięci zegara DS1307.
W następnej odsłonie kursu zobaczymy, jak zapisuje i odczytuje się pliki w MicroPythonie. Już teraz mogę jednak uchylić rąbka tajemnicy – robi się to dokładnie tak samo, jak w Pythonie, na zwykłym komputerze. Zobaczymy, co zrobić, aby mieć dostęp do plików na karcie MicroSD, a także jak zainstalować dysk w pamięci EEPROM typu 24C, która korzysta z interfejsu I²C.
Dominik Bieczyński
leonow32@gmail.com
Zobacz więcej:
- Repozytorium kursu na GitHubie https://github.com/leonow32/micropython
- Dokumentacje modułu time https://docs.micropython.org/en/latest/library/time.html
- Dokumentacja klasy RTC https://docs.micropython.org/en/latest/library/machine.RTC.html
- Dokumentacja klasy I²C https://docs.micropython.org/en/latest/library/machine.I²C.html