Skocz do zawartości

Kurs? Raspberry Pi Pico [3] - I2C, SPI


H1M4W4R1

Pomocna odpowiedź

Ten artykuł jest częścią serii "Kurs? Raspberry Pi Pico"

334385135_Template(7).thumb.jpg.db56f19cfa06026b19a5db6f626659f8.jpg

W tym rozdziale

Dowiesz się czym jest magistrala I2C oraz SPI oraz nauczysz się jak z nich korzystać przy wykorzystaniu wygodnego API 😉

I2C

Jest to drugi często spotykany rodzaj magistrali. Z niej często korzystają interfejsy dla wyświetlaczy alfanumerycznych (takich małych 16 znaków w 2 liniach) oraz np pamięci EEPROM (programowalna pamięć, którą można modyfikować z poziomu zewnętrznych urządzeń wysyłających do niej dane). To właśnie ten drugi element będzie naszym pacjentem badawczym. W moim przypadku będzie to konkretnie model AT24C64A. Zajmiemy się zapisem i odczytem jednego bajta z pamięci, do czego według specyfikacji producenta należy przesłać poprzez I2C 2 bajty adresu oraz dane do zapisania lub 2 bajty adresu i zażądać odczytu danych.

Ten artykuł bierze udział w naszym konkursie! 🔥
Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł.

konkurs_forbot_nagrody_1-350x147.png

Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu!
Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły »

Adres? Tak - I2C jest jedną z magistrali pozwalających adresować urządzenia. Do dyspozycji użytkownika jest 128 adresów (0 - 127). Wyżej wymieniona pamięć ma adres 0x50. Oprócz tego ma 3 piny pozwalające dodać do adresu maksymalnie 7 - czyli zakres adresowy jest od 0x50 do 0x57.

Zajmijmy się więc podłączeniem pamięci - do GP4 podłączamy SDA, do GP5 podłączamy SCL. Do samej kości pamięci podłączamy zasilanie oraz masę. Oprócz tego do masy zwieramy pin WP. Piny A1,A2,A3 zostawiamy “wiszące”.

ForBot_18.thumb.png.943065bb2b8314d92cf398da475e5c10.png

Teraz omówmy funkcje - tutaj również powstała wygodna biblioteka - IIC.h.

void begin(i2c_inst_t *inst, int baudRate); // Inicjacja I2C
void end(); // koniec I2C
void setBaud(int baudRate); // ustawianie baudrate I2C

int available(); // sprawdzanie czy są dane ;)
uint8_t* read(uint8_t address, size_t amount); // Odczyt danych - odczytujemy sekwencję danych binarnych
void write(uint8_t address, uint8_t* data, size_t amount); // Zapis sekwencji binarnej na SPI
void free_memory(); // Wyczyść pamięć - po wykorzystaniu odczytanych danych zalecam wykonać tę metodę - w celu oszczędności RAM’u ;)
void set_slave(bool mode, uint8_t addr); // ustawia tryb niewolnika

No i kurs został zdemonetyzowany (żart). Pewnie zastanawiasz się co to jest “tryb niewolnika” - otóż w magistrali I2C (oraz SPI, które omówimy za chwilę) istnieje master i slave. Master wysyła żądanie do slave’a, który na nie odpowiada. Czyli za początek komunikacji zawsze odpowiada master. W przypadku slave - który w naszym przykładowym programie to kość pamięci EEPROM oczekujemy na żądanie i na nie odpowiadamy.

Jeżeli ustawimy tryb na true możemy ustawić adres naszego pico i wysyłać do niego żądania tak samo jak do pamięci. Tego tematu nie będziemy poruszać w podstawowej wersji kursu, gdyż jest rzadko stosowany. Tymczasem zajmijmy się naszym programem 😉 Na początku polecam poczytać o funkcjach oraz tablicach w C/C++.

Teraz za zadanie mamy zgodnie ze specyfikacją producenta odczytać bajt do pamięci i zapisać bajt do pamięci.

By zapisać bajt zapisujemy na I2C adres komórki pamięci (dwa bajty) oraz jeden bajt, który jest wartością. W celu odczytu zapisujemy na I2C adres komórki pamięci (dwa bajty) oraz odczytujemy jeden bajt (po czasie t, który ustalmy na 10ms - jest on znacznie nad wyrost, ale i tak nie zauważymy tego, a pozwoli uniknąć błędów wynikających z tego, że kość nie zdążyła przygotować danych dla magistrali).

Przykładowy kod:

#include <cstdio>
#include "pico/stdlib.h"
#include "IIC.h"
#include <string>

void i2c_eeprom_write_byte( uint8_t addr, uint16_t mem_addr, uint8_t data ) {
  
  // Przekonweruj adres ze słowa na bajty
   uint8_t addr_msb = mem_addr >> 8;
   uint8_t addr_lsb = mem_addr & 0xFF;

   // Zbuduj tablicę do wysłania
   uint8_t data_to_write[] = {addr_msb, addr_lsb, data};

   // Zapisz bajt do pamięci EEPROM
   i2c.write(addr, data_to_write, 3);
}



uint8_t i2c_eeprom_read_byte( uint8_t addr, uint16_t mem_addr) {

   // Konwertuj adres do bajtów
   uint8_t addr_msb = mem_addr >> 8;
   uint8_t addr_lsb = mem_addr & 0xFF;

   // Zbuduj tablicę do wysłania
   uint8_t data_to_write[] = {addr_msb, addr_lsb};
   i2c.write(addr, data_to_write, 2);

   // Odczekaj chwilę
   sleep_ms(10);

   // Odczytaj zwrócone dane...
   uint8_t* data = i2c.read(addr, 1);

   return data[0]; // Zwróć pierwszy bajt
}

Kod sam się opisuje, więc za bardzo nie będę w to wnikał 😉 Na koniec przykładowy program wykorzystujący nasze funkcje:

int main() {
   stdio_init_all();
   i2c.begin(i2c0, 100*1000); // Zainicjuj I2C

   i2c_eeprom_write_byte(ADDR, 0, 0x47); // Zapisz do EEPROM[0] wartość 0x47
   sleep_ms(10); // Odczekaj chwilę ;)

   // Można i tak :D
   puts(std::to_string(i2c_eeprom_read_byte(ADDR, 0)).c_str()); // Wyślij na UART wartość EEPROM[0]
   puts(std::to_string(i2c_eeprom_read_byte(ADDR, 1)).c_str()); // Wyślij na UART wartość EEPROM[1]
   puts(std::to_string(i2c_eeprom_read_byte(ADDR, 2)).c_str()); // Wyślij na UART wartość EEPROM[2]

   // EEPROM[X] - bajt X w pamięci EEPROM
}

Jak widzimy korzystam z innej wersji wysyłania danych na UART - wersji bez biblioteki. Jeżeli masz kość EEPROM z serii AT24C możesz sam spróbować / sama spróbować i zobaczyć czy zwracane dane będą poprawne (0x47, 0x0, 0x0) lub (0x47, 0xFF, 0xFF). Oczywiście dane będą tak wyglądać, o ile nikt wcześniej nic nie zapisał na kości pamięci 😉

SPI

Trzeci rodzaj magistrali, niestety tutaj będzie bez przykładu, gdyż nie mam żadnego chipu, który mogę podpiąć pod Pico z tą magistralą (chipy mam, ale niestety wszystkie są wlutowane w płytki). No to czas pokazać, że czegoś możesz się nauczyć nawet bez praktyki.

SPI jest magistralą dupleksową, która ZAWSZE przesyła dane w obie strony. Czyli nawet jak odczytujesz dane to w tym momencie przesyłasz dane przez SPI.

W specyfikacji dostępu poprzez tę magistralę do chipu producent zwykle wymienia ustawienia: CPOL, CPHA, kolejność bitów oraz ich ilość. API też pomaga z tymi ustawieniami. Po prostu ustawiasz wartości takie jak podaje producent i nie musisz wnikać w szczegóły. Jednakowoż jeżeli chcesz wnikać - CPOL określa czy zegar w stanie standardowym ma stan wysoki czy niski, a CPHA przy jakim rodzaju zbocza zegara pobierane są dane. Warto nadmienić, że w SPI również możemy obsłużyć wiele urządzeń, do tego służy pin CS, który musi mieć (zazwyczaj) stan niski, by dany slave był aktywowany (odbierał wiadomości). Pamiętaj o tym podczas podłączania swojego chipu/urządzenia do Pico. Dodatkowo Pico w trybie Slave ma dodatkowe piny (patrz pinout w rozdziale #0), które określają czy ma odbierać wiadomości. Patrz poniższa grafika:

1-21.thumb.jpg.7f124e2ba8776e0ca6a6229ed5b119ce.jpg

W naszym API pinami SPI0 są odpowiednio piny GP16 - MISO, GP18 - SCK, GP19 - MOSI. I tego możemy się trzymać.

Funkcje dostępne w API to:

SPI* begin(spi_inst_t *inst, int baudRate); // Inicjuje SPI ;)
SPI* cpha(bool isHigh); // Ustawia CPHA
SPI* cpol(bool isHigh); // Ustawia CPOL
SPI* data_bits(uint8_t bits); // Ustawia ilość bitów
SPI* msb_first(); // MSB_FIRST
SPI* lsb_first(); // LSB_FIRST
void setup(); // aktualizuje ustawienia
void end(); // Konczy pracę SPI
void setBaud(int baudRate); // Ustawia baudrate
uint8_t read(); // odczytuje bajt wysyłając 0x0
void write(uint8_t data); // wysyła bajt
uint8_t read_write(uint8_t data); // równocześnie wysyła i odczytuje bajt (dupleks)
void set_slave(bool mode); // Włącza tryb slave

Przykładowy syntetyczny program może wyglądać następująco:

#include <cstdio>
#include "pico/stdlib.h"
#include "SPI.h"

SPI spi;

int main() {
   stdio_init_all();
   spi.begin(spi0, 12000000)->cpol(false)->cpha(false)->data_bits(8)->msb_first()->setup(); // Inicjacja SPI ;) 

   spi.write(0x40); // Wyślij komendę 0x40
   for(int q = 0; q < 4; q++) {
       spi.write(0x0); // Odczekaj 4 cykle wysyłając pustą treść
   }  

   uint8_t data = spi.read(); // Odczytaj dane
}

Jest to syntetyczny program, który nie robi zupełnie nic, ale pokazuje jak korzystać z API. Teraz wystarczy, że znajdziesz w swojej szufladzie coś korzystającego z magistrali SPI, podążysz za wskazaniami producenta i gotowe - komunikacja SPI z Pi Pico.

To by było na tyle w tym rozdziale

Jeżeli masz jakieś pytania śmiało je zadawaj, od tego tutaj jesteśmy 😉

Zadania domowe

  1. Przećwicz magistralę I2C
  2. Przećwicz magistralę SPI
  • Lubię! 1
Link do komentarza
Share on other sites

Po wypiciu melisy udało mi się skonfigurować VS i kompiluje kod w "Developer Command Prompt". Miganie diodami i użycie przycisku poszło gładko, ale już podłączenie wyświetlacza po I2C to masakra, nic nie działa. Nie mam pojęcia co ja źle robię. Przykłady też mi nie działają. Najgorzej, że w necie pełno jest przykładów wytłumaczonych na micropythonie a w C++ brakuje.

Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.