Magistrala I²C służy do przesyłania danych (komunikacji) pomiędzy urządzeniami elektronicznymi, a dokładniej pomiędzy układem nadrzędnym (Master) oraz urządzeniami typu Slave. Magistrala I²C jest oparta o transmisję szeregową prowadzoną za pomocą dwóch linii sygnałowych SDA (linia przesyłu danych) oraz SCL (sygnał zegarowy). Najczęściej występującym rodzajem transmisji jest zastosowanie jednego układu Master oraz kilku układów Slave. Rozpoznawanie układów w magistrali realizowane jest poprzez nadanie im unikalnego adresu, który użytkownik może modyfikować sprzętowo (łącząc odpowiednie piny układu z masą lub napięciem zasilania).
Możemy wyróżnić dwa rodzaje obsługi magistrali I²C: sprzętową oraz programową. W tej części zajmiemy się pierwszym wymienionym rodzajem. Do tego celu możemy wykorzystać mikrokontrolery wyposażone w moduł zwany TWI (np. ATmega8).
Do rozpoczęcia komunikacji musimy spełnić 3 istotne warunki:
- posiadać układy: Master oraz Slave (w naszym przypadku będzie to ATmega8 oraz PCF8591 (8-bitowy, 4-kanałowy przetwornik analogowo-cyfrowy i cyfrowo-analogowy),
- prawidłowo je połączyć ze sobą,
- napisać podstawowe funkcje.
O ile pierwszy warunek bardzo łatwo spełnić, to drugi punkt może sprawić pewne problemy. Przede wszystkim należy odnaleźć piny, które mogą służyć do obsługi magistrali I²C. Zaglądamy do not katalogowych:
ATmega8_datasheet
PCF8591_datasheet
Jak widać musimy połączyć ze sobą: pin PC5 ATmegi z 10. nóżką przetwornika oraz pin PC4 z nóżką 9. Niestety nie możemy tego dokonać bezpośrednio. Obie linie należy połączyć rezystorem podciągającym (ja używam rezystorów o wartości 4,7 kΩ) do napięcia zasilania – tak jak jest to przedstawione na schemacie poniżej:
Mając połączone ze sobą układy (oczywiście zakładam, że pozostałe, niezbędne połączenia są również wykonane poprawnie) możemy przejść do punktu 3. Napiszmy podstawowe funkcje obsługi magistrali I²C dla mikrokontrolera. Krok pierwszy – oczywiście zaglądamy do noty katalogowej naszej ATmegi i szukamy rejestrów związanych z modułem TWI (w załączonej wcześniej nocie rozdział przeznaczony magistrali I²C rozpoczyna się na stronie 157). Znajduje się tam obszerny opis standardu transmisji, który warto sobie w wolnej chwili poczytać. Do obsługi podstawowych funkcji będziemy poruszać się w granicach dwóch rejestrów TWCR oraz TWDR:
Teraz możemy zabrać się za napisanie podstawowych funkcji:
Funkcja rozpoczęcia transmisji:
void I2C_start(void){ TWCR = (1<<TWINT) | (1<<TWEN) |( 1<<TWSTA); while (! (TWCR & (1<<TWINT))); }
Aby rozpocząć transmisję najpierw zerujemy flagę TWINT ustawiając ten bit rejestru na wartość 1. Następnie odblokowujemy interfejs TWI, ustawiając bit TWEN. Jako ostatni na wartość 1 ustawiamy bit TWSTA, którym aktywujemy wygenerowanie sekwencji startu transmisji w trybie Master. Po wykonaniu tych operacji w pętli while oczekujemy na ponowne ustawienie flagi TWINT, co potwierdzi, że układ sprzętowy wykonał prawidłowo sekwencję startu transmisji.
Funkcja zakończenia transmisji:
void I2C_stop(void){ TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO); while (! (TWCR & (1<<TWSTO))); }
Tutaj analogicznie jak powyżej: aby zakończyć transmisję najpierw zerujemy flagę TWINT ustawiając ten bit rejestru na wartość 1. Następnie odblokowujemy interfejs TWI, ustawiając bit TWEN. Jako ostatni na wartość 1 ustawiamy bit TWSTO, którym aktywujemy wygenerowanie sekwencji STOP. Po wykonaniu tych operacji w pętli while oczekujemy na ustawienie flagi TWSTO, co potwierdzi, że układ sprzętowy wykonał prawidłowo zakończył transmisję.
Funkcja wysłania jednego bajtu
void I2C_write(uint8_t dane){ TWDR = dane; TWCR = (1<<TWINT) | (1<<TWEN); while (! (TWCR & (1<<TWINT))); }
Najpierw do rejestru TWDR wpisujemy bajt, który chcemy przesłać. Następnie odblokowujemy przerwanie TWINT oraz interfejs TWI (ustawienie bitu TWEN). Później grzecznie czekamy w pętli na potwierdzenie zakończenia operacji poprzez ustawienie flagi przerwania TWINT.
Funkcja odbioru jednego bajtu
uint8_t I2C_read(uint8_t ACK){ TWCR = (1<<TWINT) | (ACK<<TWEA) | (1<<TWEN); while (! (TWCR & (1<<TWINT))); return TWDR; }
Odczytując bajt z magistrali bardzo istotną rolę odgrywają potwierdzenia (ACK). Jeżeli chcemy odczytać tylko jeden bajt, to problem potwierdzeń nas nie dotyczy. Natomiast przy odczycie kilku bajtów, musimy zadbać o odpowiednie generowanie tych sygnałów (na razie nas to nie interesuje). Dla nas ważne jest, aby po odbiorze ostatniego (w naszym przypadku pierwszego i jedynego) bajtu ustawić bajt ACK na wartość 0, co będzie sygnałem dla układu Slave, aby odłączył się od magistrali. Patrząc na kod, widzimy, że przesyłamy wartość ACK jako zmienną przekazywaną do funkcji – takie podejście będzie nam potrzebne później przy konstrukcji funkcji odczytującej wiele bajtów z magistrali. Funkcja działa analogicznie, jak wcześniej przedstawione. Ważną różnicą jest to, że po zakończeniu odczytu (sprawdzenie ustawienia flagi w pętli) zwraca ona wartość rejestru TWDR jako zmienną typu uint8_t.
Literatura:
Kardaś M.: Mikrokontrolery AVR. Język C. Podstawy programowania., Wyd. Atnel, Szczecin 2011.
Francuz T.: Język C dla mikrokontrolerów AVR. Od podstawi do zaawansowanych aplikacji., Wyd. Helion, Gliwice 2011.