Kurs STM32 – #11 – I2C w praktyce, pamięć eeprom

Kurs STM32 – #11 – I2C w praktyce, pamięć eeprom

Wcześniej poznaliśmy dwa interfejsy szeregowe: UART i SPI. Pierwszy wymagał tylko dwóch linii, ale był dość powolny. Drugi działał bardzo szybko wykorzystując więcej wyprowadzeń.

Teraz zajmiemy się I2C, który kwalifikuje się gdzieś pomiędzy wcześniejszymi interfejsami.

Na początku wykorzystamy I2C (czytaj: i kwadrat ce) do komunikacji z pamięcią EEPROM, a później z akcelerometrem. Przykład z pamięcią EEPROM jest bardzo ważny.

Główny bohater odcinka - pamięć EEPROM.

Główny bohater odcinka - pamięć EEPROM.

I2C - Najważniejsze informacje

Interfejs I2C podobnie do SPI działa w sposób synchroniczny. Oznacza to, że sygnał zegarowy jest przesyłany między układami, co odróżnia go od UART-a, który musiał polegać na zegarach wbudowanych w komunikujące się układy.

Projektanci interfejsu postanowili ograniczyć liczbę niezbędnych linii, ale jednocześnie umożliwić podłączanie wielu układów do wspólnego interfejsu. Wymagania na pierwszy rzut oka mogą się wydawać sprzeczne. Jednak ostatecznie założony cel został osiągnięty (niestety kosztem większej komplikacji protokołu).

Tak samo jak w przypadku SPI układy komunikujące się przez I2C dzielą się na 2 kategorie: układów nadrzędnych (ang. master) oraz podrzędnych (ang. slave). Do jednego interfejsu może być jednocześnie podłączonych kilka układów nadrzędnych, my skupimy się na prostej wersji z jednym układem master, którym będzie nasz mikrokontroler.

Zestaw elementów do kursu

 999+ pozytywnych opinii  Gwarancja pomocy  Wysyłka w 24h

Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!

Zamów w Botland.com.pl »

Podłączenie magistrali I2C

Schemat podłączenia układów do I2C wygląda następująco:

Magistrala I2C.

Magistrala I2C.

Jak widać wykorzystywane są dwie linie komunikacyjne (Vdd, to zasilanie):

  • SDA - dwukierunkowa linia danych
  • SCL - linia zegarowa sterowana przez układ nadrzędny

W przypadku SPI do przesyłania danych wykorzystywane były dwie linie danych MOSI i MISO. Dzięki temu jednocześnie można było wysyłać i odbierać informacje. Taki tryb działania nazywany jest full-duplex. I2C posiada tylko jedną linię danych, więc w danej chwili można na niej tylko wysyłać lub odbierać dane.

Najczęściej wyjścia mikrokontrolera są używane w tzw. trybie przeciwsobnym. Jeśli na wyjściu jest logiczne 0, odpowiednie wyprowadzenie jest zwierane do masy, logiczna 1 natomiast to połączenie z zasilaniem (3.3V).

W takiej sytuacji połączenie wyjść dwóch układów może łatwo prowadzić do zwarcia. Jeśli jeden układ będzie próbował wystawić logiczne 0, a drugi 1, nastąpi zwarcie między masą i zasilaniem. Aby tego uniknąć konieczna jest synchronizacja dostępu do interfejsu i zapobieganie jednoczesnym zapisom do wspólnej linii. W przypadku SPI do takiej synchronizacji były wykorzystywane linie CS - tylko jeden układ slave mógł w danym momencie sterować linią MISO.

Ponieważ I2C nie wykorzystuje sygnałów CS, konieczne było znalezienie innej metody współdzielenia linii. Wykorzystano w tym celu wyjście typu otwarty kolektor (open drain). Oznacza to, że gdy mikrokontroler wystawia na pinie logiczne 0, linia jest zwierana do masy. Natomiast logiczna 1 zamiast zwierać wyście do zasilania, pozostawia linię niepodłączoną - dokładnie tak samo, jak podczas pracy linii jako wejścia.

Sposób działania linii magistrali I2C

Jeśli którykolwiek z układów podłączonych do danej linii interfejsu wystawi logiczne 0, cała linia jest zwierana do masy. Natomiast kiedy wszystkie układy wystawiają wartość 1, linia za pomocą rezystora jest łączona z zasilaniem i pojawia się na niej napięcie 3.3 V.

Kolejne etapy komunikacji przez I2C

Komunikacja przez I2C wygląda następująco:

  1. Układ nadrzędny rozpoczyna komunikację poprzez wystawienie sygnału START - jest to specjalna kombinacja przebiegów na liniach SDA i SCL informująca o początku transmisji.
  2. Następnie wysyła adres układu z którym chce się komunikować oraz bit informujący o kierunku transmisji (nadawanie albo odbiór). Układ podrzędny odpowiada wysyłając bit potwierdzenia ACK.
  3. Po wybraniu układu, następuje właściwa transmisja:
    1. jeśli zostało wybrane nadawanie, master wysyła kolejne bajty, a slave potwierdza odebranie każdego bitem ACK
    2. w przypadku odbioru, slave wysyła kolejne bajty, a master odbiera i potwierdza za pomocą ACK
  4. Na koniec master wysyła sygnał STOP informujący o zakończeniu transmisji

Są jeszcze dwie rzeczy, które zostały pominięte w powyższym opisie - jeśli master chce zacząć kolejną transmisję natychmiast po poprzedniej, może pominąć końcowe STOP i od razu wysłać kolejne START.

Druga sprawa to potwierdzenia - pisałem o ACK, ale istnieje jeszcze negatywne potwierdzenie NACK. Najczęściej informuje ono o końcu danych, przykładowo master odczytując dane powinien potwierdzać odbiór za pomocą ACK, poza ostatnim bajtem, kiedy powinien odesłać NACK tak, aby slave wiedział, że nie musi już więcej danych wysyłać.

Gdy wiemy już jak działa ten nowy (w naszym kursie) interfejs możemy przejść do praktyki. Teraz zajmiemy się pamięcią eeprom, a w kolejnym odcinku przyjdzie pora na akcelerometr cyfrowy.

STM32_EEPROM

Pamięć EEPROM

Jako pierwszy przykład wykorzystamy komunikację z pamięcią EEPROM, układem typu 24AA01. Jak zawsze należy zacząć od przeczytania dokumentacji. Jest to relatywnie mała pamięć nieulotna o pojemności 128B. Może się to wydawać niewiele w dobie dysków o pojemności TB, jednak taka pamięć jest zaskakująco użyteczna.

Oczywiście w mikrokontrolerze znajdziemy dużo więcej pamięci nieulotnej typu Flash, którą moglibyśmy wykorzystać do przechowywania danych. Jednak pamięć Flash ma dość ograniczoną liczbę cykli zapisu. W przypadku STM32F103 producent deklaruje 10000 cykli zapisu, więc jeśli chcielibyśmy w tej pamięci przechowywać dane, które często zmieniamy, moglibyśmy szybko przekroczyć ten limit. Dla porównania - nasza pamięć EEPROM wytrzyma aż 1000000 cykli zapisu. Poza tym łatwiej ją wymienić niż cały mikrokonotroler.

Przyjrzyjmy się wyprowadzeniom układu 24AA01:

Opis wyprowadzeń pamięci eeprom.

Opis wyprowadzeń pamięci eeprom.

Linie Vss i Vcc to odpowiednio masa i zasilanie. SCL i SDA to linie naszego interfejsu I2C. WP oznacza Write Protect i musi być połączona z masą, jeśli chcemy mieć możliwość zapisu do pamięci EEPROM. Są jeszcze trzy wyprowadzenia adresowe A0, A1, A2 - nie są one używane w układzie 24AA01 i mogą pozostać niepodłączone.

Podłączenie układu

Układ powinniśmy podłączyć zgodnie z poniższym rysunkiem.

i2c-eeprom_bb

Do komunikacji z pamięcią EEPROM wykorzystamy interfejs I2C1. Jego linie SCL i SDA są dostępne na pinach PB6 i PB7.

Zapis do pamięci

Schemat komunikacji z układem 24AA01 znajdziemy we wspomnianej wcześniej dokumentacji. Poniżej umieściłem najważniejszy dla nas datagram:

Sposób zapisu do pamięci EEPROM przez I2C.

Jak widzimy, jest to typowa komunikacja za pomocą I2C. Najpierw wysyłamy sygnał START. Po nim adres urządzenia wraz z bitem kierunku transmisji. Przy nadawaniu (zapisie) jest to zero, a przy odbiorze (odczycie) jedynka.

Układ pamięci odpowiada przesyłając potwierdzenie ACK. Następnie wysyłamy adres komórki pamięci, do której chcemy zapisywać. Pamiętamy przy tym, że dostępnych jest (aż) 128 adresów. Pamięć ponownie potwierdza odbiór danych za pomocą ACK.

Następnie wysyłamy jedną lub więcej wartości, każda jest potwierdzana za pomocą ACK. Na koniec wysyłamy STOP, aby zakończyć transmisję i umożliwić pamięci rzeczywisty zapis danych.

STM32 i EEPROM - zapis danych w praktyce

Wiemy już jak ma wyglądać transmisja, teraz możemy przejść do jej implementacji na naszym mikrokontrolerze. Jak zwykle zaczynamy od podłączenia odpowiedniego sygnału zegarowego:

Konfigurujemy piny PB6 i PB7 do pracy z interfejsem I2C. Tryb którego użyjemy to open-drain:

Kolejny krok to zadeklarowanie zmiennej konfigurującej i2c oraz inicjalizacja modułu. Jest to dość proste, ponieważ w większości wykorzystamy domyślne opcje:

Wybrana prędkość, to 100kHz. Maksymalna dla naszego układu wynosi 400kHz, jednak wtedy dość długie przewody łączące mogłyby spowodować problemy z komunikacją.

Aby rozpocząć transmisję, wysyłamy sygnał START i czekamy, aż I2C wykona nasze polecenie:

Następnie wysyłamy adres (identyfikator) układu i czekamy na odpowiedź:

Teraz możemy przesłać adres komórki pamięci, do której będziemy chcieli zapisywać dane. Przykładowo zapiszmy je pod adresem zerowym:

Dalej wysyłam jedną lub więcej wartość. Adres zapisywanej komórki pamięci będzie automatycznie zwiększany, więc do komórki o adresie 0x00 zapiszmy 0xCA, a do 0x01 wartość 0xFE:

Ponieważ w naszym przykładzie nic więcej nie robimy, możemy zakończyć komunikację wysyłając sygnał STOP:

Pełny kod powyższego przykładu zamieszczony jest poniżej:

Program niestety nie da widocznych efektów. Aby sprawdzić jego działanie musimy nauczyć się odczytywać dane.

STM32 i EEPROM - odczyt danych w praktyce

W poprzednim przykładzie zapisaliśmy przykładowe dane 0xCA oraz 0xFE do pamięci. Teraz spróbujemy je odczytać i sprawdzimy, czy wartości się zgadzają. Ponownie, przebieg odczytu jest opisany w dokumentacji układu pamięci. Odpowiedni diagram widzimy poniżej:

Sposób odczytu do pamięci EEPROM przez I2C.

Najpierw musimy ustalić adres, spod którego wczytamy dane. Wymaga to przesłania danych z mastera do slave-a, więc musimy najpierw wykonać zapis. Wysyłamy więc START, następnie adres układu oraz adres komórki pamięci, którą chcemy odczytać.

Poprzednio omówiliśmy już potrzebne instrukcje:

Teraz układ 24AA01 w wewnętrznym rejestrze zapamięta wartość 0x00 jako adres, spod którego będziemy mogli odczytywać dane. Ponieważ chcemy zmienić kierunek komunikacji musimy zacząć od początku, czyli wysłania sygnału START:

Następnie przesyłamy identyfikator układu pamięci, ale z ustawionym bitem odczytu. Nie musimy ręcznie modyfikować adresu, funkcja I2C_Send7bitAddress() zajmie się tym za nas, jeśli podamy kierunek transmisji I2C_Direction_Receiver:

Dodatkowo włączyliśmy odsyłanie potwierdzeń ACK do układu slave. Więc będzie on przysyłał kolejne bajty, aż wyślemy STOP lub wyłączymy potwierdzenia.

Wczytujemy pierwszy odebrany bajt:

Podobnie moglibyśmy wczytywać kolejne, poza ostatnim, ponieważ po nim nie powinniśmy odsyłać potwierdzenia - jak widzimy przed STOP powinniśmy zamiast ACK odpowiedzieć NACK.

Piszemy więc odpowiedni kod:

Całość wygląda następująco:

Jak sprawdzić, czy program działa? Najlepiej uruchomić go pod debuggerem i zobaczyć czy zawartość zmiennych data1 i data2 jest zgodna z oczekiwaniami. O ile poprzednio zapisaliśmy dane, a nasz układ jest poprawnie podłączony powinniśmy zobaczyć rezultat jak na obrazku:

I2C_02

Zapis i odczyt do pamięci EEPROM w praktyce.

Licznik uruchomień układu

Umiemy już zapisywać i odczytywać dane z pamięci EEPROM, czas napisać program, który wykorzysta naszą nową umiejętność. Napiszmy program, który będzie zliczał ile razy został uruchomiony. Informacje o liczbie uruchomień będziemy wysyłać za pomocą UART-a.

Poprzednio pisaliśmy program do komunikacji z EEPROM-em w głównej funkcji main, teraz przygotujmy odpowiednie funkcje pomocnicze.

Po pierwsze zarówno zapis, jak i odczyt zaczyna się tak samo - od ustawienia adresu pamięci, do którego chcemy mieć dostęp. Możemy więc wydzielić ten kod jako procedurę:

Korzystając z niej, zapis do pamięci jest znacznie łatwiejszy:

Jak widzimy, procedura jest bardziej uniwersalna niż poprzednio, może zapisywać kilka bajtów na raz. Podobnie możemy napisać procedurę do odczytu danych:

Jest ona nieco bardziej skomplikowana ze względu na inne traktowanie ostatniego odbieranego bajtu. Mając odpowiednie procedury, program główny jest już bardzo prosty:

Zamiast adresu 0x00 tym razem wykorzystywany jest adres 16, czyli 0x10. Dzięki temu nie skasujemy danych zapisanych wcześniej. Całość programu wygląda następująco:

Rezultat działania programu w praktyce widoczny jest poniżej:

I2C_01

STM32 i EEPROM w praktyce - licznik uruchomienia programu.

Podsumowanie

Komunikacja za pomocą I2C jest nieco bardziej skomplikowana niż za pomocą SPI, czy UART, jednak interfejs ten ma wiele zalet. Za pomocą raptem dwóch linii można połączyć wiele urządzeń i komunikować się z nimi z zadowalającą prędkością.

Komunikacja może w pierwszej chwili wydawać się skomplikowana, ale raz napisane procedury mogą być równie łatwo używane w przykładowym programie, jak i w przypadku innych rozwiązań.

Nawigacja kursu

Autor kursu: Piotr (Elvis) Bugalski
Redakcja: Damian (Treker) Szymański

eeprom, i2c, interfejs, komunikacja, kursSTM32, stm32