sobota, 3 kwietnia 2021

STM32G0 - I2C , uruchomienie sterownika dotyku CAP1296 firmy Microchip .

Zadanie jakie sobie wytyczyłem tym wpisem to zapoznanie się z interfejsem I2C w STM32G0 . Ale aby zapoznanie nie było nudne, spróbuję uruchomić przy okazji sterownik dotyku Microchipa, CAP1296. W STM32G0 w wersji 32 pin, której używam mamy do dyspozycji dwa interfejsy I2C. Przy czym ten drugi czyli I2C2 jest trochę wykastrowany z funkcjonalności w stosunku do pierwszego czyli I2C1. Ale nie będzie to przedmiotem moich dociekań. Oba interfejsy oprócz standardowych prędkości 100 i 400 kHz mają możliwość pracy z prędkością 1 Mbit/s.
 
Poniżej wymieniam sprzęt i orpogramowanie jakim będę się posługiwał :
 
  • Moja płytka z STM32G071KBT6 (płytki jeszcze fizycznie nie mam) 

 

  • Moduł ze sterownikiem dotyku CAP1296 firmy Microchip. Płytka w standardzie MikroBus.

  • Analizator stanów logicznych

 
  •  Programator JLink Mini Edu firmy Segger
 
  • Przewód ze złączem IDC20 o rastrze 2.54 do programatora
 

 
  •  Embedded Studio for ARM firmy Segger
 

Całą zabawę wypadałoby zacząć od konfiguracji pinów na potrzeby interfejsu I2C.
Ja użyję I2C2 czyli tego wykastrowanego :) i pinów PA11 (SCL) , PA12 (SDA). Te piny mam w złączu MikroBus nr 2 na mojej płytce.

Konfigurację pinów należy zacząć od włączenia zegara dla modułu GPIO Port A. Zrobimy to przy pomocy rejestru RCC_IOPENR zapisem jak poniżej :

RCC->IOPENR |= RCC_IOPENR_GPIOAEN;
 
Powyższy zapis wynika z tego co mamy w pliku nagłówkowym w sekcji dotyczącej rejestru RCC_IOPENR
 

Korelujemy to oczywiście z samym rejestrem bo to z jego zawartości wynika co i gdzie trzeba ustawić :
 




Drugim zegarem jaki trzeba włączyć jest oczywiście zegar dla modułu I2C2.
Stosowne ustawienie trzeba zaaplikować do rejestru RCC_APBENR1. Najpierw zerkamy sobie w zawartość rejestru :
 
 

a potem w plik nagłówkowy  stm32g071xx.h i fragment dotyczący rejestru RCC_APBENR1
 

Konkludujemy stosowny zapis włączający zegar dla modułu I2C2 czyli ustawiamy bit nr 22 na wartość 1:
 
RCC->APBENR1 |= RCC_APBENR1_I2C2EN ; 

Kolejnym krokiem będzie konfiguracja pinów PA11 i PA12 do współpracy z modułem I2C2. W tym celu wchodzimy sobie w zakładkę GPIO w Manual Reference i zaczynamy naszą podróż od rejestru ustawiającego sposób wykorzystania pinów czyli GPIOx_MODER i tam w polach odpowiadających naszym pinom czyli MODE11 i MODE12 musimy wpisać wartość 0b10 (Alternate fuction mode)


Oczywiście podpieramy się zapisami w pliku nagłówkowym stm32g071xx.h
 
 
Wiemy zatem co chcemy ustawić , gdzie to możemy ustawić i jakie definicje mamy do dyspozycji aby to w miarę czytelnie zapisać, więc stwórzmy odpowiednie zapisy :
 
/* ustaw starszy bit pola bitowego MODE11 i MODE12 na 1 */
GPIOA->MODER |= (GPIO_MODER_MODE11_1 | GPIO_MODER_MODE12_1);
 
/* ustaw młodszy bit pola bitowego MODE11 i MODE12 na 0  */
GPIOA->MODER &= ~(GPIO_MODER_MODE11_0 | GPIO_MODER_MODE12_0);
 
Jak się przekonać czy powyższe zapisy działają i czy są prawidłowe ?? O tym się przekonamy jak przyjdzie mi płytka i będę mógł nasz MCU potraktować ultra szybkim debuggerem Seggera. W sumie to zaraz, zaraz czyżbym się nie mógł już doczekać :).
 
Teraz pozostałe punkty konfiguracji pinów czyli , typ konfiguracji elektrycznej pinów, szybkość narastania zbocza , pull up oraz przyporządkowanie pinów do modułu I2C2. No cóż w MCU Microchipa było łatwiej, bo po przyporządkowaniu pinów do modułu I2C , moduł sam sobie konfigurował piny. Na tym prostym przykładzie można się przekonać, że STM32 w odróżnieniu od Microchipa ma mniej zaawansowane peryferia nawet w prostych modułach.
 
Ustawiamy dla PA11 i PA12, typ konfiguracji elektrycznej jako Otwarty Dren (Open drain), czyli w rejestrze GPIOA_OTYPER w polach OT11OT12 wpisujemy wartość 1
 
/* ustaw bit pola bitowego OT11 i OT12 na 1 */
GPIOA->OTYPER |= (GPIO_OTYPER_OT11 | GPIO_OTYPER_OT12) ;

Tu musimy sobie zapamietać, że piny, które konfigurujemy w interfejsach komunikacyjnych a w szczególności I2C muszą pracować jako open drain a nie push-pull.
 
Kolejne ustawienie dla PA11 i PA12 to Pull up, czyli w rejestrze GPIOA_PUPDR w polach PUPD11PUPD12 wpisujemy wartość 0b01
 
/* ustaw młodszy bit pola bitowego PUPD11 i PUPD12 na 1 */
GPIOA->PUPDR |= (GPIO_PUPDR_PUPD11_0 | GPIO_PUPDR_PUPD12_0) ;
 
/* ustaw starszy bit pola bitowego PUPD11 i PUPD12 na 0  */
GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD11_1 | GPIO_PUPDR_PUPD12_1);
 
A teraz największe wariactwo w STM32 czyli szybkość narastania zbocza. W rejestrze GPIOA_OSPEEDR w polach OSPEED11 i OSPEED12 wpiszemy wartość 0b01 (Low speed), bo nie chcemy aby została tam wartość domyślna czyli Very low speed
 
/* ustaw młodszy bit pola bitowego OSPEED11 i OSPEED12 na 1 */
GPIOA->OSPEEDR |= (GPIO_OSPEEDR_OSPEED11_0 | GPIO_OSPEEDR_OSPEED12_0) ;
 
/* ustaw starszy bit pola bitowego OSPEED11 i OSPEED12 na 0  */
GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED11_1 | GPIO_OSPEEDR_OSPEED12_1) ;
 
Dochodzimy do ostatniego punktu konfiguracji czyli przyporządkowania naszych pinów PA11 i PA12 do modułu I2C2. Do tego celu służą rejestry GPIOA_AFRL i
GPIOA_AFRH. Zajrzyjmy do środka jednego z nich , który będzie nas interesował :

W szczególności, nas będą interesowały pola AFSEL11 i AFSEL12. Zauważmy, że pola są 4-bitowe , więc tutaj najpierw wyzerujemy całe pole a potem wprowadzimy odpowiednie zmiany. Do naszych pól musimy wprowadzić wartość 0b0110 co reprezentuje magiczną funkcjonalność opisaną jako AF6, cóż to takiego jest. W tym miejscu należy udać się do datasheet naszego MCU gdzie na stronie 48 mamy taką tabelkę z której połapiemy się w mig o co chodzi z tym AF6 pamiętając , że nasze piny to PA11 i PA12 :
 

Znając już metodologię zapisów do rejestru i patrząc tylko na nazwę rejestru i nazwę pól nie musimy nawet zaglądać do pliku nagłówkowego aby dokonać odpowiednich zapisów. I to już zaczyna być fajne. Najpierw kasujemy całe pole bitowe AFSEL11 i AFSEL12. I tu się trochę pospieszyłem z oceną sytuacji co do zapisu :) bo w przypadku rejestru AFR posługujemy się tablicą jak poniżej , taki mały haczyk ale szybko można się połapać o co chodzi na podstawie komunikatów kompilatora :

/* wyzeruj pola bitowe AFSEL11 i AFSEL12  */
GPIOA->AFR[1] &= ~(GPIO_AFRH_AFSEL11 | GPIO_AFRH_AFSEL12) ;
 
a teraz ustawiamy pola bitowe AFSEL11 i AFSEL12 na 0b0110
 
/* ustaw pola bitowe AFSEL11 na 0b0110*/
GPIOA->AFR[1] |= (GPIO_AFRH_AFSEL11_1 | GPIO_AFRH_AFSEL11_2) ;

/* ustaw pola bitowe AFSEL12 na 0b0110*/
GPIOA->AFR[1] |= (GPIO_AFRH_AFSEL12_1 | GPIO_AFRH_AFSEL12_2) ;
 
Ostatni zapis rozbiłem na dwa , tak chyba jest czytelniej niż jednym ciągiem.
 
No i zarąbiście, mamy wyczerpany temat konfiguracji pinów PA11 i PA12 do współpracy z modułem I2C2. Jak przyjdzie płytka to sprawdzę powyższe zapisy czy efekt zamierzony pokrywa się z rzeczywistością :). Mając na uwadze ile funkcjonalności naustawialiśmy za jednym zamachem :) to ilość zapisów nie jest zbyt przerażająca :).
 
No i teraz mała refleksja, gdybym swoją edukację w MCU wystartował w HAL, nie umiejąc kompletnie poruszać się po rejestrach to powiem tak , dupa blada. Bo teraz chcąc się przesiąść na MCU innego producenta muszę poznać znowu jego bibliotekę typu HAL i ponownie uczyć się czegoś co nie jest kompletnie rozwojowe w sensie samorozwoju. Nabywając umiejętności poruszania się po rejestrach np. zaczynąć od AVR to potem przeskoczenie na bardziej zaawansowane rejestry 32-bitowe jest jakąś naturalną drogą rozwoju. I z taką powiedzmy to uniwersalną wiedzą wejście na inny MCU innego producenta to pikuś. Każdy ma pewnie swoje doświadczenia w tym względzie ja jednak uważam, że HAL oki ale tylko wtedy jeśli poruszanie się po rejestrach jest już w naszym zbiorze umiejętności. Howgh !.
 
Od tej chwili poruszać się będziemy po module I2C czyli zaczynamy konfigurację tego modułu. Jedynym dokumentem jakim się tutaj posługujemy to Manual Reference dla naszego MCU. Dokument taki nie czytamy od deski do deski tylko wybiórczo to co nas w danej chwili interesuje. Na stronie 937 mamy od STM-a prezent w postaci schematu blokowego dla inicjalizacji modułu I2C.


Najpierw STM proponuje nam wyzerować bit PE w rejestrze I2C_CR1. Zaglądamy zatem do tego rejestru i patrzymy o co kaman.



Bit PE wyłącza nam moduł I2C i powoduje wewnętrzny reset tego modułu.  Zwracam tutaj uwagę na to, że po wyzerowaniu bitu PE w rejestrze I2C_CR1 należy poczekać 3 cykle zegara , żeby ten reset się uprawomocnił. No dobra więcej gadania niż pisania, twórzmy zapis :

/* ustaw bit PE na 0*/
I2C2->CR1 &= ~I2C_CR1_PE ;

I ponownie nie musiałem nawet zaglądać do pliku nagłówkowego aby ten zapis wykontycypować. Szybko załapiemy nomenklaturę zapisu jak parę razy to przećwiczymy. Warunkiem koniecznym jest tylko zerknięcie na nazwę rejestru , w jakim jest module i jak nazywa się pole bitowe, które nas interesuje i z ilu bitów się składa.

Kolejnym elementem konfiguracji I2C wynikającym ze schematu blokowego jest ustawienie w rejestrze I2C_CR1 pola bitowego DNF i ANFOFF. No i znowu, żeby się dowiedzieć o co biega wchodzimy w ten rejestr i opis tych pól bitowych.

 
STM zafundował nam tutaj filtr analogowy i cyfrowy dla modułu I2C.  W przypadku prędkości 100 kHz dla I2C, filtrów nie musimy brać pod uwagę ale dla trybów 400 kHz i 1Mbit/s już tak. Choć jak zerknąłem to konfigurator w STM32 CUBE IDE załącza filtr analogowy już i dla 100 kHz. To chyba jakaś słaba strona STM32 ,skoro potrzebuje filtrować sygnał przed podaniem sygnału na  peryferium. U Microchipa żadnych manualnych filtrów nie zobaczymy w I2C nawet dla prędkości 3.4 Mbit/s (patrz np. PIC32CM). 

Ale żeby nie było nudno włączmy sobie filtr analogowy.

/* ustaw bit ANFOFF na 1*/
I2C2->CR1 |= I2C_CR1_ANFOFF ;

W schemacie blokowym inicjalizacji I2C od STM-a brakuje mi takich podstawowych informacji jak wybór trybu Master/Slave , prędkości czyli np. 100 kHz czy trybu adresacji 7/10 bit. No nic może jak dojdziemy do kolejnego punktu na schemacie czyli konfiguracja magicznego rejestru I2C_TIMINGR to coś nam się rozjaśni. 

Zaglądając do rejestru I2C_TIMINGR trochę mi kopara opadła. W rejestrze tym musimy wyliczyć i ustawić timingi przebiegów na magistrali I2C uwzględniając m.in taktowanie zegara, prędkość I2C i wiele innych zależności :). Jest jednak uprzejma informacja, że STM32Cube IDE wylicza to automatycznie. Poszedłem tym tropem :



Na szaro oznaczony fragment Timing gdzie widzimy wartość 0x00303D5B, to efekt wyliczenia i tą wartość należy wpisać do rejestru I2C_TIMINGR. Wartość ta jest właściwa dla zegara 16MHz i prędkości 100kHz. Jeśli coś zmienimy w taktowaniu to wartość ta ulegnie zmianie. 

/* ustaw TIMINGR*/
I2C2->TIMINGR 0x00303D5B ;

Zauważmy, że nie mamy jawnego wyboru trybu Master/Slave w konfiguratorze , więc te funkcjonalności pewnie gdzieś w sposób pośredni są uzyskiwane.

Kolejny element na schemacie blokowym to rejestr I2C_CR1 i pole bitowe o nazwie NOSTRETCH. Jak się dowiadujemy z opisu tego bitu dla trybu Master ten bit musi być ustawiony na 0.

/* ustaw bit NOSTRETCH na 0*/
I2C2->CR1 &= ~I2C_CR1_NOSTRETCH ;

Zwieńczeniem inicjalizacji I2C w oparciu o schemat blokowy z manuala jest zapis włączający moduł I2C, w naszym przypadku wyglądać on będzie jak poniżej :

/* ustaw bit PE na 1*/
I2C2->CR1 |= I2C_CR1_PE ;

Pozostaje mi teraz czekać na płytkę i zweryfikować wszystkie zapisy czy wchodzą do rejestrów.

Po inicjalizacji I2C czas zająć się już konkretnymi funkcjami realizującymi zapis i odczyt do/z sterownika dotyku CAP1296. Poniżej szablony komunikacji jakie muszę zrealizować :



Rzut okiem na mój zestaw startowy , na płytce wpięty moduł dotyku z CAP1296 :


Podstawowy program jaki uruchomiłem na początek celem sprawdzenia komunikacji I2C w STM32G0 to odczyt rejestru CAP1296 o adresie 0xFD (Product ID) gdzie spodziewana wartość w odczycie to 0x69. Poniżej zaprezentuję jak wykonałem inicjację I2C, konfigurację pinów i funkcję realizującą odczyt i zapis danej z/do CAP1296

Inicjalizacja PA11 i PA12 do współpracy z I2C2.


Zegary :



Inicjalizacja I2C2 :


Funkcja dedykowana do odczytu rejestru z CAP1296 :
 

Przykład użycia : 
I2C2_ReadRegister_CAP1296(CAP1296_ADDR, CAP1296_SENSTATUS) 
 
Funkcja dedykowana do zapisu danej do rejestru w CAP1296 :


Przykład użycia :
I2C2_WriteRegister_CAP1296(CAP1296_ADDR, CAP1296_INTENABLE , 0x03);

Przykładowe zrzuty z analizatora stanów odczyt i zapis do CAP1296 :




W linku zapodam program na GitHubie, program realizuje prostą interakcję z dotykiem. Dotknięcie jednego pola na płytce aktywuje diodę LED na pinie PA8, dotknięcie drugiego pola gasi diodę LED. Identyczny przykład jak pokazywałem w artykule wcześniejszym z PIC18F47Q84.

Odnośnie funkcji I2C jakie poczyniłem do rozmowy z CAP1296. Należy zauważyć , że jest to baza nad którą trzeba jeszcze popracować w zakresie np. wprowadzenia mechanizmów nieblokujących. Widziałem w rejestrach, że jakiś mechanizm jest tutaj serwowany np. w I2C_TIMEOUTR. Do dyspozycji mamy jeszcze przerwania, DMA. Zauważyłem jednak , że coraz więcej programistów opiera działanie interfejsów komunikacyjnych o mechanizm maszyny stanów.

Ogólnie należy stwierdzić, że oprogramowanie rejestrami I2C w STM32G071 nie jest szczególnie trudne tym bardziej, że mamy do dyspozycji w dokumentacji schematy blokowe transmisji. Intensywne ćwiczenia na rejestrach dały mi fajną naukę, m.in dotyczącą przesunięć bitowych, zauważyłem , że im więcej obcuje się z rejestrami tym więcej się nabywa takich małych umiejętności. Kiedyś musiałem chwilę się zastanowić jak zrobić jakąś bardziej wyrafinowaną operację bitową teraz widzę ją oczami wyobraźni i bity mi się w głowie przesuwają i od razu widzę wynik :), genialne uczucie kiedy widzimy coś głębiej i szybciej niż standardowo. Wyostrzają mi się zmysły :)

Zdaję sobie jednak sprawę , że trendy są nieubłagane i odchodzi się od pracy organicznej na rejestrach na rzecz bibliotek wysokopoziomowych odseparowanych od warstwy sprzętowej. Ten trend zabija kreatywność w programistach i programowanie może przestać sprawiać satysfakcję. Wszystko jest podane na tacy nic nie trzeba wytężać umysłu, nuda ... Programowanie nie jest już przygodą.  Ludzie w takim komformistycznym otoczeniu szybko wypalają się zawodowo, przestaje im się coś chcieć. Jaka z tego będzie przyszłość ano taka, że programiści w ogóle nie będą potrzebni bo zastąpią ich automaty programujące wspomagane sztuczną inteligencją :)

Taka refleksja jeszcze odnośnie środowiska STM32 CUBE, w konfiguratorze nie mamy możliwości podglądu rejestrów a sam wygenerowany kod stara się jak może te rejestry ukryć przed nami operując często na samych adresach w pamięci. Moim zdaniem to bardzo duża wada tego środowiska. W MPLABX IDE bardzo ceniłem sobie w konfiguratorze MCC możliwość podejrzenia zmian w rejestrach po wyklikaniu jakiegoś ustawienia sprzętowego. Dawało mi to jakiś głębszy obraz sytuacji . Kod wygenerowany w STM32 CUBE jest w/g mnie znacznie gorszej jakości niż serwuje Microchip w swoich konfiguratorach. W STM32 CUBE na jedną wygenerowaną linijkę kodu przypada pierdylion zakomentowanych miejsc , które musimy respektować do pisania własnego kodu. Jeśli kod napiszemy poza wyznaczonymi miejscami to zniknie on po kolejnym wygenerowaniu projektu z konfiguratora.



Pozdrawiam

PICmajster
picmajster.blog@gmail.com
 
Linki :


4 komentarze:

  1. Wesołych Świąt Wielkanocnych , smacznego jajka ;-)

    OdpowiedzUsuń
  2. Dziękuję Witek i wszystkiego dobrego Tobie. Smacznych jajek mam codziennie w brud z racji posiadania 15 szt kur + 1 szt koguta :).

    OdpowiedzUsuń
  3. Naprawdę fajny artykuł z wieloma ciekawymi informacjami. Bardzo fajnie się czyta. Ostatnio trafiłem na temat UART https://www.micros.com.pl/uklady-scalone/uklady-interfejsowe/uart/ Może poruszysz taki temat. Ostatnio interesuje się tym tematem, także jestem ciekaw informacji od Ciebie. Pozdrawiam

    OdpowiedzUsuń