Podstawowa obsługa przetwornika ADC
Podobnie jak większość rodzin układów STM32, także mikrokontrolery Megawin z serii MG32F1 są wyposażone w 12-bitowy przetwornik analogowo-cyfrowy (rysunek 1). Zarówno sposób konfiguracji programowej, jak i zakres funkcjonalności tego bloku okazują się bardzo zbliżone w przypadku STM32F1 i MG32F103 – dlatego osoby zaznajomione z użyciem starych bibliotek STM32 Standard Peripheral Library będą pozytywnie zaskoczone podobieństwem kodu źródłowego, zaprezentowanego na listingu 1, do programów obsługujących ADC w procesorach STM32F1.
/* wlaczenie sygnalu taktowania ADC i niezbednych zegarow dodatkowych */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_ADC | RCC_APB1Periph_BMX1| RCC_APB1Periph_AFIO, ENABLE);
/* odblokowanie bloku kontrolnego funkcji analogowych w celu zapisu */
PWR_UnlockANA();
/* zezwolenie na prace przetwornika ADC */
ANCTL_SARADCCmd(ENABLE);
/* przywrocenie stanu zablokowanego w celu ochrony waznych rejestrow*/
PWR_LockANA();
/* struktura inicjalizacyjna ADC */
ADC_InitTypeDef init;
/* uzywamy pojedynczej konwersji, wiec nie potrzebujemy trybu ciaglego skanowania */
init.ADC_ScanConvMode = DISABLE;
init.ADC_ContinuousConvMode = DISABLE;
/* nie uzywamy sprzetowego triggera (wyzwalanie programowe) */
init.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
/* wyrownanie danych do prawej (zera po stronie MSB) */
init.ADC_DataAlign = ADC_DataAlign_Right;
/* jeden kanal do konwersji */
init.ADC_NbrOfChannel = 1;
/* inicjalizacja przetwornika */
ADC_Init(&init);
/* uzywamy tylko kanalu nr 14, wybieramy mozliwie dlugi czas probkowania */
ADC_RegularChannelConfig(ADC_Channel_14, 1, ADC_SampleTime_239Cycles5);
/* zezwolenie na wyzwalanie przetwornika */
ADC_ExternalTrigConvCmd(ENABLE);
/* wlaczenie bloku przetwornika */
ADC_Cmd(ENABLE);
/* zerowanie ustawien kalibracyjnych */
ADC_ResetCalibration();
/* oczekiwanie na gotowosc do rozpoczecia kalibracji */
while(ADC_GetResetCalibrationStatus());
/* rozpoczecie kalibracji */
ADC_StartCalibration();
/* oczekiwanie na zakonczenie kalibracji */
while(ADC_GetCalibrationStatus());
}
Listing 1. Funkcja konfigurująca i inicjalizująca przetwornik ADC
Pewne wątpliwości może natomiast budzić blok trzech instrukcji, znajdujących się na początku ciała funkcji void ADC_config():
ANCTL_SARADCCmd(ENABLE);
PWR_LockANA();
Choć same nazwy wywołanych tutaj procedur na dobrą sprawę tłumaczą ich działanie, to kilka słów wyjaśnienia należy się Czytelnikom w nawiązaniu do samej istoty bloku ANCTL oraz powodów, dla których został on wyposażony w mechanizmy ochrony przed zapisem. W dokumentacji producenta nie znajdziemy schematu blokowego tego modułu – jest to wszak po prostu zestaw rejestrów konfiguracyjnych – ale jego znaczenie w funkcjonowaniu procesorów marki Megawin jest niebywale istotne. Blok ANCTL zarządza bowiem (na najwyższym poziomie funkcjonalnym) pracą nie tylko modułów typowo analogowych (przetwornika ADC oraz komparatora), ale także struktur MCU o znaczeniu krytycznym w kontekście niezawodności całego systemu, czyli:
- oscylatorów MHSI (8 MHz), FHSI (48 MHz), LSI (32 kHz), HSE (4...16 MHz, w zależności od zastosowanego kwarcu lub generatora zewnętrznego),
- pętli synchronizacji fazowej (PLL),
- kontrolera pamięci Flash,
- bloku resetu POR/PDR,
- bloku nadzorującego napięcie zasilania (PVD).
Sensowne jest zatem zastosowanie blokady przed przypadkowym nadpisaniem wartości rejestrów kontrolujących pracę tych – kluczowych dla bezpieczeństwa – bloków procesora. Dlatego też, aby móc skorzystać z wbudowanego przetwornika analogowo-cyfrowego, musimy najpierw odblokować możliwość zapisu do rejestrów ANCTL, dopiero wtedy zezwolić na działanie ADC, po czym (dla pewności) ponownie zablokować ANCTL.
Następne operacje bazują już wyłącznie na rejestrach samego przetwornika i polegają na przygotowaniu zawartości struktury inicjalizacyjnej typu ADC_InitTypeDef, na drodze sukcesywnego przypisywania nastaw kolejnych parametrów. Potem – przy użyciu funkcji ADC_Init(), przyjmującej jako parametr wskaźnik na ww. strukturę – wystarczy już tylko przekazać odpowiednie ustawienia do bloku rejestrów konfiguracyjnych. Osobnych działań wymaga:
- wybór kanału(-ów) do skanowania przez sekwencer wbudowany w przetwornik ADC – oraz czasu próbkowania,
- zezwolenie na wyzwalanie przetwornika sygnałem zewnętrznym (w naszym przypadku – dla ułatwienia – zastosujemy wyzwalanie programowe),
- właściwe włączenie bloku przetwornika (nie należy mylić tej operacji z odgórnym zezwoleniem, które ustawiliśmy wcześniej w bloku ANCTL),
- przeprowadzenie automatycznej kalibracji ADC, mającej na celu zniwelowanie rozrzutów pojemności próbkujących (producent zaleca wykonanie kalibracji każdorazowo po włączeniu zasilania systemu).
Tak skonfigurowany przetwornik ADC jest całkowicie przygotowany do pracy. Teraz, już w funkcji main() naszego programu, wystarczy rozpocząć konwersję (funkcja ADC_SoftwareStartConvCmd(ENABLE)), odczekać na jej zakończenie (poprzez sprawdzenie flagi EOC funkcją ADC_GetFlagStatus(ADC_FLAG_EOC)), a następnie odczytać zawartość 16-bitowego rejestru ADC->DR. Całość jest wykonywana w pętli for(), mającej na celu uśrednianie zestawu próbek (w naszym przypadku używamy „bufora” o rozmiarze 128), dzięki czemu odczyty prezentowane na wyświetlaczu LED będą znacznie stabilniejsze niż przy pracy z pojedynczymi wynikami konwersji ADC.
Wnikliwi Czytelnicy zauważą, że we fragmencie programu głównego, pokazanym na listingu 2, znalazła się także instrukcja warunkowa sprawdzająca stan przycisku SW2 i (w zależności od niego) wywołująca (lub nie) dodatkową funkcję ADC_GetADValue(), która jako parametr przyjmuje skopiowaną do zmiennej adc_tmp zawartość rejestru wyjściowego przetwornika.
{
int16_t cnt = 0;
uint8_t pos = 0;
/* liczba probek ktore beda usredniane */
const uint8_t num_samples = 128;
/* zmienna do przechowywania wyniku konwersji */
uint16_t adc_tmp = 0;
/* akumulator do usredniania probek */
uint32_t volt_avg = 0;
/* zmienna przechowujaca liczbe do wyswietlenia */
uint16_t disp_num = 0;
initPeripherals();
LED_onoff(1, LED_OFF);
LED_onoff(2, LED_OFF);
while (1)
{
volt_avg = 0;
/* zapelnianie bufora probek i sumowanie wynikow "w locie" */
for(uint8_t i = 0; i < num_samples; i++){
/* rozpoczecie pojedynczej konwersji ADC */
ADC_SoftwareStartConvCmd(ENABLE);
/* polling flagi konca konwersji */
while(ADC_GetFlagStatus(ADC_FLAG_EOC) != SET){}
/* przeliczanie wyniku na mV (Vref = Vcc = 3,3 V) */
adc_tmp = ADC->DR;
/* nacisniecie SW2 powoduje zastosowanie "funkcji korygujacej" */
if(!GPIO_ReadInputDataBit(SW2_port, SW2_pin))
adc_tmp = ADC_GetADValue(adc_tmp);
volt_avg += (adc_tmp * 3300) / 4096;
}
/* zakonczenie obliczen (srednia arytmetyczna) */
volt_avg /= num_samples;
int8_t digits[4];
/* Pozyskanie kolejnych pozycji dziesietnych */
digits[0] = (disp_num % 10000) / 1000;
digits[1] = (disp_num % 1000) / 100;
digits[2] = (disp_num % 100) / 10;
digits[3] = (disp_num % 10);
/* Tymczasowe wylaczenie wyswietlacza */
display_select_pos(255);
/* Ustawienie cyfry do wyswietlenia na katodach */
display_select_digit(digits[pos], (pos==0)?1:0);
/* Wlaczenie wybranej pozycji */
display_select_pos(pos);
/* Obsluga licznika pozycji dziesietnych */
pos++;
if(pos > 3){
pos = 0;
disp_num = (uint16_t)volt_avg;
}
}
}
Listing 2. Główna funkcja programu do obsługi przetwornika ADC
I tutaj właśnie zachodzi zjawisko dość dziwne, które w dokumentacji producenta nie zostało zbyt obszernie opisane. Ciało tejże funkcji pokazano na listingu 3.
{
uint32_t correct_value;
uint32_t c_k, c_a;
if(data<=64&&data>=32)
{
return data-32;
}
else if(data<=0x1F)
{
return data+4064;
}
else
{
correct_value = (data – 32) & 0xFFF;
c_k = 133;
c_a = 53;
correct_value = ((correct_value + c_a) * 10000) / (c_k + 10000);
return correct_value < 4096 ? correct_value : 4095;
}
}
Listing 3. Funkcja korygująca odczyty z przetwornika ADC
Jak widać, funkcja ADC_GetADValue() ma za zadanie zwrócić nam (w postaci wartości typu uint16_t) „skorygowany wynik konwersji”, a korekcja ta zależy od przedziału, w którym znajduje się przekazany do funkcji parametr. Dokładniejsze przyjrzenie się warunkom oraz wykonywanym po ich spełnieniu operacjom matematycznym sugeruje, że opisywana funkcja ma za zadanie niwelować stosunkowo spore błędy offsetów, ale także (za co odpowiada ostatni warunek else) – pewną nieliniowość przetwarzania. Można się jedynie domyślać, że genezy takiego rozwiązania należy upatrywać w (wykrytych zbyt późno przez producenta) błędach w projekcie przetwornika na poziomie „krzemu”. Taki scenariusz wydaje się wysoce prawdopodobny – tym bardziej że dziesiątki rozmaitych uchybień jest znanych także w przypadku procesorów znacznie większych i bardziej doświadczonych producentów półprzewodników (nieprzypadkowo pliki erraty, opracowywane np. do procesorów STM32, są całkiem obszerne i zawierają szereg mniej lub bardziej istotnych błędów zidentyfikowanych po wprowadzeniu układów do sprzedaży).
Kompletny kod źródłowy znajduje się w materiałach dodatkowych do tego odcinka kursu, dostępnych na stronie ep.com.pl. W katalogu Projekt03 umieszczone zostały wszystkie pliki i foldery niezbędne do uruchomienia projektu w środowisku Keil.
Po skompilowaniu i wgraniu kodu maszynowego do pamięci Flash procesora możemy przetestować nasz program – na wyświetlaczu LED będą się ukazywały wyniki konwersji przeliczone na wartość napięcia. Na marginesie warto dodać, że funkcja odpowiedzialna za wybór segmentów wyświetlacza niezbędnych do włączenia na danej pozycji (void display_select_digit(uint8_t dig, uint8_t dp)) została tym razem wzbogacona o zapis:
GPIO_SetBits(DISP_DP_port, DISP_DP_pin);
}else{
GPIO_ResetBits(DISP_DP_port, DISP_DP_pin);
}
który na podstawie wartości parametru dp zaświeca lub gasi punkt dziesiętny obok aktualnie obsługiwanego znaku (pozycji) wyświetlacza.
Warto we własnym zakresie dokładnie przetestować działanie programu – i to zarówno przy wciśniętym, jak i przy zwolnionym przycisku SW2. Podczas testów na płytce prototypowej dało się zauważyć, że funkcja korekcyjna ADC_GetADValue() istotnie zmienia nieznacznie liniowość odczytu oraz wpływa na offsety, ale nie redukuje całkowicie problemu „zawijania” zakresu pomiarowego. Dlatego też – w skrajnych położeniach suwaka potencjometru – wartości napięcia są obsługiwane błędnie: czy to poprzez nagły przeskok do drugiej skrajności (np. z 0 do ponad 4000), czy to przez efekt „saturacji”, gdy obrót gałki o niewielki kąt nie powoduje żadnych zmian w odczycie napięcia. Praktyka pokazuje zatem, że przetwornik analogowo-cyfrowy wbudowany w procesor MG32F103 nie należy do najdokładniejszych i nie powinien być używany do pomiarów, w których duże znaczenie ma wiarygodność odczytów. Nic jednak nie stoi na przeszkodzie, by korzystać z niego w mniej wymagających zastosowaniach, np. do zgrubnej kwantyzacji wejść analogowych czy też do obsługi prostych czujników (np. fotorezystora w aplikacji wyłącznika zmierzchowego).
SysTick i przerwania
Do tej pory dostępny na naszej płytce ewaluacyjnej wyświetlacz LED obsługiwaliśmy w najprostszy (i – z programistycznego punktu widzenia – najgorszy) możliwy sposób, czyli poprzez skanowanie kolejnych pozycji (znaków) w pętli głównej programu. Teraz naprawimy ten (intencjonalny) grzech uproszczenia, wprowadzając obsługę multipleksowania za pomocą przerwań od timera systemowego SysTick, stanowiącego integralną część mikrokontrolerów z rdzeniem ARM (niezależnie od producenta docelowej implementacji) i przeznaczonego przede wszystkim do taktowania mechanizmów czasowych systemów operacyjnych. Nic nie stoi jednak na przeszkodzie, by w aplikacjach typu bare-metal skorzystać z tego przydatnego peryferium w innych zastosowaniach, np. właśnie do obsługi multipleksowanego wyświetlacza LED.
W bibliotekach dostarczonych przez firmę Megawin dostępna jest prosta w użyciu funkcja konfigurująca timer SysTick o nazwie SysTick_Config(uint32_t ticks); co ważne, funkcja ta pochodzi z biblioteki CMSIS, stąd jej zastosowanie jest takie samo, niezależnie od producenta mikrokontrolera. Osobom nieposiadającym doświadczenia w programowaniu procesorów o architekturze ARM przyda się informacja, że parametr ticks przekazywany w wywołaniu funkcji oznacza po prostu liczbę taktów sygnału zegarowego, która ma być odliczona pomiędzy kolejnymi wywołaniami procedury obsługi przerwania (ISR) od SysTicka. Aby uzyskać najczęściej spotykaną wartość równą 1 kHz, musimy zatem obliczyć parametr ticks poprzez podzielenie częstotliwości sygnału doprowadzonego do SysTicka (wyrażonej w hercach) przez nasz docelowy 1 kiloherc. Rdzeń mikrokontrolera w używanej przez nas konfiguracji jest taktowany sygnałem HCLK o częstotliwości 72 MHz pochodzącym z pętli PLL, zatem parametr ticks przyjmie wartość 72 000.
Warto w tym miejscu dodać, że w mikrokontrolerach z rodziny MG32F103 można wybrać także taktowanie SysTicka sygnałem HCLK podzielonym przez 8 – w tym celu, tuż po wykonaniu funkcji SysTick_Config(), należy wywołać dodatkową procedurę SysTick_CLKSourceConfig() z parametrem SysTick_CLKSource_HCLK_Div8, zdefiniowaną w pliku misc.c. Znacznie bardziej przydatna okazuje się natomiast funkcja rekonfigurująca priorytet przerwania – NVIC_SetPriority(), zdefiniowana w standardowym pliku źródłowym przeznaczonym do architektury ARM Cortex-M3 (core_cm3.h). Domyślne wywołanie SysTick_Config() zakłada, że priorytet przerwania od timera systemowego będzie ustawiony na najniższą możliwą wartość, stąd dowolne przerwanie o priorytecie nawet nieznacznie wyższym może bez trudu wywłaszczyć procedurę jego obsługi. Kwestia ustalenia priorytetów ma oczywiście znaczenie w przypadku, gdy w systemie korzystamy z wielu różnych przerwań – jak na razie SysTick jest jedynym peryferium, któremu zezwalamy na generowanie przerwań, stąd możemy pozostać przy ustawieniach domyślnych.
Ciało ISR timera SysTick należy umieścić w pliku mg32f10x_it.c, koniecznie pamiętając o wstawieniu deklaracji tejże procedury w odpowiadającym pliku nagłówkowym mg32f10x_it.h. Ważne jest tutaj dokładne odwzorowanie nazwy ISR, która swoje umocowanie logiczne ma w tablicy wektorów przerwań, zawartej w pliku startup_mg32f10x.lst. Procedura SysTick_Handler() przejmie od programu głównego zadanie obsługi wyświetlacza multipleksowanego, należy jedynie pamiętać o umieszczeniu zmiennych globalnych poza ciałem funkcji main() w pliku main.c, a także o umieszczeniu stosownych odwołań do tychże zmiennych w procedurze ISR. Wygląd funkcji SysTick_Handler(void) można zobaczyć na listingu 4.
/* odwolania do zmiennych globalnych sterujacych multipleksem */
extern volatile uint8_t pos;
extern volatile uint16_t disp_num;
extern volatile uint16_t disp_num_shadow;
int8_t digits[4];
/* pozyskanie kolejnych pozycji dziesietnych */
digits[0] = (disp_num_shadow % 10000) / 1000;
digits[1] = (disp_num_shadow % 1000) / 100;
digits[2] = (disp_num_shadow % 100) / 10;
digits[3] = (disp_num_shadow % 10);
/* tymczasowe wylaczenie wyswietlacza */
display_select_pos(255);
/* ustawienie cyfry do wyswietlenia na katodach */
display_select_digit(digits[pos], (pos==0)?1:0);
/* wlaczenie wybranej pozycji */
display_select_pos(pos);
/* obsluga licznika pozycji dziesietnych */
pos++;
if(pos > 3){
pos = 0;
/* aktualizacja odczytu po pelnym cyklu multipleksu */
disp_num_shadow = disp_num;
}
}
Listing 4. Procedura obsługi przerwania od SysTicka
Słowa komentarza wymaga jeszcze istotna zmiana wprowadzona w obsłudze samego multipleksu, a odróżniająca nieco sposób działania tego algorytmu od wersji zaprezentowanej w poprzednich programach. Tym razem – z uwagi na wyższą częstotliwość odświeżania LED oraz jego niezależność od biegu programu głównego – konieczne okaże się wprowadzenie dodatkowego zabezpieczenia przed przekłamaniami w odczytach. Zawartość zmiennej volt_avg jest modyfikowana w każdym obiegu pętli uśredniającej próbki pobierane z rejestru wyjściowego ADC. Na koniec całego procesu przepisujemy wprawdzie wynik obliczeń do zmiennej disp_num, ale wciąż nie daje nam to gwarancji, że swobodnie wykonywana procedura ISR nie pomiesza układów segmentów wyświetlacza odpowiadających następującym po sobie wynikom pomiaru. Dlatego też w programie wprowadzono podwójne buforowanie – ISR przepisuje zawartość zmiennej disp_num do „kopii zapasowej”, czyli zmiennej disp_num_shadow dopiero po zakończeniu całego cyklu odświeżania wyświetlacza LED. W ten sposób zyskujemy pewność, że wskazanie prezentowane na wyświetlaczu będzie zawsze zgodne z rzeczywistym wynikiem pomiaru. Nieco podobne zabezpieczenie, ale z pojedynczym buforowaniem, można zobaczyć także na listingu 2 – tam jednak, z uwagi na bardziej synchroniczne (i zarazem deterministyczne) wykonywanie kodu obsługującego pomiary ADC oraz odświeżanie wyświetlacza, takie rozwiązanie w zupełności wystarczało.
Obsługa interfejsu I²C – konfiguracja
Na koniec tej części naszego kursu pozostawiliśmy obsługę wbudowanego interfejsu I²C mikrokontrolera MG32F103, którego uproszczony schemat blokowy pokazano na rysunku 2.
Jak przystało na nowoczesny procesor z rdzeniem ARM, blok I²C naszego procesora oferuje szereg udogodnień programistycznych, zabezpieczeń i automatycznych funkcji nadzorujących oraz korygujących działanie interfejsu szeregowego. W piątym projekcie omawianym na łamach naszego kursu skupimy się na obsłudze zewnętrznego czujnika temperatury typu MCP9808 firmy Microchip – na płytce ewaluacyjnej jest on obecny jako układ U5.
Pierwszy element, który musimy dodać do naszego programu, to konfiguracja portów PB10 i PB11 jako linii interfejsu I²C (SCL i SDA):
GPIO_Init(GPIOB, GPIO_Pin_11, GPIO_MODE_AF |GPIO_OTYPE_OD | GPIO_PUPD_NOPULL |GPIO_SPEED_HIGH |GPIO_AF4);
Warto zwrócić uwagę, że tym razem wybieramy tryb wyjścia jako OD (otwarty dren), NOPULL (brak wewnętrznych rezystorów podciągających) oraz SPEED_HIGH (wysoka dopuszczalna częstotliwość przełączania).
W tej samej funkcji initPeripherals() wywołujemy także procedurę inicjalizacji interfejsu I²C – I2C_config() – której ciało pokazano na listingu 5.
{
/* wlaczenie taktowania interfejsu I²C i odpowiedniego sygnalu szyny systemowej */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_BMX2 | RCC_APB2Periph_I2C2, ENABLE);
/*
PB10 (I²C2_SCL)
PB11 (I²C2_SDA)
*/
/* wstepne zerowanie konfiguracji I²C */
I2C_DeInit(I2C2);
/* - tryb mastera
- standardowa szybkosc transferu
- zabezpieczenie szyny przed bledami ("zatrzasniecie" SDA lub SCL na stanie niskim)
- zezwolenie na powtorzony start
*/
I2C_Init(I2C2, I2C_CON_SLAVE_DISABLE | I2C_CON_SPEED_STANDARD | I2C_CON_MASTER_MODE | I2C_CON_BUS_CLEAR_FEATURE_CTRL | I2C_CON_RESTART_EN);
/* ustawienie adresu slave’a */
I2C_TargetAddressConfig(I2C2, 0x18);
/* parametry timingow linii SDA i SCL */
/* tHIGH = (280 + FS_SPKLEN + 7) / 72 MHz = 4 us */
I2C2->SS_SCL_HCNT = 280;
/* tLOW = (339 + 1) / 72 MHz = 4,708 us */
I2C2->SS_SCL_LCNT = 339;
/* tSP = 3 / 72 MHz = 41,67 ns */
I2C2->FS_SPKLEN = 3;
/* tSU;DAT = 18 / 72 MHz = 250 ns */
I2C2->SDA_SETUP = 18;
/* tHD;DAT = 22 / 72 MHz = 302,08 ns */
I2C2->SDA_HOLD = 22;
/* I²C SCL Stuck at Low Timeout = 720000 / 72 MHz = 10 ms */
I2C2->SCL_STUCK_AT_LOW_TIMEOUT = 720000;
}
Listing 5. Procedura inicjalizacji interfejsu I²C
Po włączeniu taktowania interfejsu I²C, w odpowiednim rejestrze RCC należy wykonać deinicjalizację tego bloku peryferyjnego, co pozwoli niejako wystartować od zera, a tym samym uniknąć potencjalnych problemów podczas konfiguracji. Zaraz w następnym kroku dokonujemy inicjalizacji interfejsu, stosując poniższe ustawienia:
- tryb: master I²C,
- szybkość transferu danych: standardowa (około 100 kHz),
- aktywne zabezpieczenie szyny przed błędami (o tej funkcji napiszemy więcej w dalszej części artykułu),
- zezwolenie na generowanie przez mastera sygnału powtórzonego startu.
Następnie ustalamy adres slave’a, który w przypadku czujnika MCP9808 (podłączonego w konfiguracji sprzętowej zastosowanej na naszej płytce ewaluacyjnej) ma wartość 0x18. Ostatnią czynnością jest ustawienie rejestrów odpowiedzialnych za timingi stosowane podczas komunikacji – kolejne linie kodu odpowiadają za parametry:
- tHIGH – czas trwania stanu wysokiego w sygnale SCL,
- tLOW – czas trwania stanu niskiego w sygnale SCL,
- FS spike suppression limit (FS_SPKLEN) – liczba cykli sygnału taktującego blok I²C odpowiadająca najdłuższemu impulsowi zakłócającemu, który zostanie stłumiony przez filtr cyfrowy,
- SDA setup time – czas, o jaki moment stabilizacji stanu na linii SDA ma wyprzedzać zbocze narastające sygnału na linii SCL,
- SDA hold time – czas, przez który prawidłowy stan logiczny sygnału na linii SDA ma być utrzymywany po zboczu opadającym na linii SCL,
- SCL stuck at low timeout – liczba cykli sygnału taktującego, po upłynięciu których kontroler I²C ma wygenerować przerwanie, jeżeli linia SCL pozostaje przez cały czas zablokowana (tj. panuje na niej stan niski).
Rzeczywiste przebiegi zarejestrowane na tak skonfigurowanej linii zegarowej można zobaczyć na oscylogramie pokazanym na rysunku 3.
Jak już wspomnieliśmy wcześniej, inżynierowie firmy Megawin odpowiedzialni za projektowanie procesorów przewidzieli dość rozbudowane opcje automatycznego uwalniania szyny po wystąpieniu na niej krytycznych błędów transmisji, objawiających się ściągnięciem linii SDA lub SCL do masy. Funkcja nazwana Bus Clear Feature (rysunek 4), jeżeli została aktywowana za pomocą maski I2C_CON_BUS_CLEAR_FEATURE_CTRL w parametrze wywołania funkcji I2C_Init(), samoczynnie monitoruje obydwie linie szyny I²C – i w razie potrzeby dokonuje resetu całego interfejsu.
Komunikacja z czujnikiem MCP9808
Uzbrojeni w najważniejsze informacje dotyczące konfiguracji I²C w trybie mastera, możemy przejść do opisu właściwej komunikacji z czujnikiem MCP9808. Na początek spójrzmy na rysunek 5 pokazujący schematycznie przebiegi na liniach SCL i SDA podczas odczytu rejestru TA.
Dla ułatwienia nie będziemy zagłębiać się w tajniki konfiguracji i rozmaite funkcje dodatkowe sensora, skupimy się jednie na podstawowym odbiorze danych na temat zmierzonej temperatury. Po wysłaniu bajtu adresowego zakończonego bitem W (zapis) master powinien nadać adres rejestru, który w przypadku TA ma wartość 0x05.
Następnie master wystawia stan ponowionego startu i dokonuje standardowego odczytu dwóch bajtów danych w kolejności od MSB do LSB.
W rzeczywistym projekcie należałoby oddzielić obsługę samego czujnika od funkcji nadających i odbierających bajty do/z slave’a. Ponieważ jednak naszym celem jest poznanie samego mikrokontrolera, a nie pisanie uniwersalnego kodu o poziomie abstrakcji umożliwiającym jego przenoszenie pomiędzy różnymi platformami, to (wyjątkowo) całości odczytu dokonamy za pomocą pojedynczej funkcji int16_t mcp9808_read_temp(void). Ciało tej funkcji pokazano na listingu 6.
uint32_t err = 0;
uint8_t raw[2];
/* uruchomienie interfejsu I²C */
I²C_Cmd(I²C2, ENABLE);
/* zerowanie flag przerwan */
I²C_ClearITPendingBit(I²C2, 0xFFFF);
/* adres rejestru temperatury (TA) */
I²C_WriteDataCmd(I²C2, 0x05);
/* przejscie w tryb odczytu */
I²C_WriteDataCmd(I²C2, I²C_DATA_CMD_READ);
/* oczekiwanie na gotowosc */
while( (I²C_GetFlagStatus(I²C2, I²C_FLAG_RFNE) == RESET) &&
(I²C_GetRawITStatus(I²C2, I²C_IT_TX_ABRT) == RESET) &&
(I²C_GetRawITStatus(I²C2, I²C_IT_SCL_STUCK_AT_LOW) == RESET));
/* odczyt bajtu danych (MSB) */
raw[0] = I²C_ReadData(I²C2);
/* po drugim odczycie ma byc wyslany sygnal stopu */
I²C_WriteDataCmd(I²C2, I²C_DATA_CMD_READ | I²C_DATA_CMD_STOP);
/* oczekiwanie na gotowosc */
while( (I²C_GetFlagStatus(I²C2, I²C_FLAG_RFNE) == RESET) &&
(I²C_GetRawITStatus(I²C2, I²C_IT_TX_ABRT) == RESET) &&
(I²C_GetRawITStatus(I²C2, I²C_IT_SCL_STUCK_AT_LOW) == RESET));
/* odczyt bajtu danych (LSB) */
raw[1] = I²C_ReadData(I²C2);
/* oczekiwanie na gotowosc */
while( (I²C_GetFlagStatus(I²C2, I²C_FLAG_TFE) == RESET) &&
(I²C_GetRawITStatus(I²C2, I²C_IT_TX_ABRT) == RESET) &&
(I²C_GetRawITStatus(I²C2, I²C_IT_SCL_STUCK_AT_LOW) == RESET));
/* oczekiwanie na zakonczenie pracy maszyny stanow mastera */
while(I²C_GetFlagStatus(I²C2, I²C_FLAG_MST_ACTIVITY) != RESET);
/* sprawdzenie flag ewentualnych bledow nadawania */
if(I²C_GetRawITStatus(I²C2, I²C_IT_TX_ABRT) != RESET) {
/* zwrocenie informacji o przyczynie bledu nadawania */
err = I²C_GetTxAbortSource(I²C2);
/* zerowanie flagi przerwania od bledu nadawania */
I²C_ClearITPendingBit(I²C2, I²C_IT_TX_ABRT);
}
else if(I²C_GetRawITStatus(I²C2, I²C_IT_SCL_STUCK_AT_LOW) != RESET) {
/* arbitralnie przyjeta wartosc bledu oznaczajaca blokade SCL stanem niskim */
err = 0xFFFFFFFF;
/* zerowanie flagi przerwania od bledu zegara */
I²C_ClearITPendingBit(I²C2, I²C_IT_SCL_STUCK_AT_LOW);
}
I²C_Cmd(I²C2, DISABLE);
/* zwrocenie arbitralnie przyjetej wartosci oznaczajacej blad odczytu */
if(err) return 9999;
/* przeliczenie wartosci wyjsciowej na temperature w [*C]x100 */
raw[0] &= 0x1F;
return ((raw[0] * 1600) + (raw[1]*100 / 16));
}
Listing 6. Funkcja odczytu danych ze scalonego czujnika temperatury MCP9808
Po uruchomieniu interfejsu i wyzerowaniu flag przerwań dokonujemy zapisu pierwszego bajtu danych – korzystamy tutaj z gotowej funkcji I2C_WriteDataCmd(), której pierwszym parametrem jest uchwyt używanego przez nas bloku peryferyjnego (w tym przypadku I2C2), zaś drugi to właściwe dane do przesłania. W kolejnej linii znów stosujemy tę samą funkcję, tym razem jednak jej parametrem jest z góry ustalona przez producenta komenda – I2C_DATA_CMD_READ. Zaraz, zaraz… czy na pewno wszystko jest tutaj w porządku? W większości innych mikrokontrolerów rejestr danych służy wyłącznie do realizacji zapisu, odczytu lub obydwu tych funkcji, ale nie do sterowania przepływem przez generowanie stanów „specjalnych” na linii I²C…
Istotnie – rozwiązanie zastosowane przez inżynierów firmy Megawin nie należy wprawdzie do najbardziej intuicyjnych, ale w gruncie rzeczy okazuje się całkiem wygodne w użyciu. Aby dokładnie zrozumieć sposób odróżniania właściwych danych od komend sterujących, należy zajrzeć do pliku mg32f10x.h, w którym zdefiniowano komendy pokazane na listingu 7.
#define I²C_DATA_CMD_DAT_Msk (0xFFU) /*!< DAT field mask bit */
#define I²C_DATA_CMD_READ (0x1U << 8) /*!< This bit controls whether a read or a write is performed */
#define I²C_DATA_CMD_STOP (0x1U << 9) /*!< This bit controls whether a STOP is issued after the byte
is sent or received */
#define I²C_DATA_CMD_RESTART (0x1U << 10) /*!< This bit controls whether a RESTART is issued before the byte
is sent or received */
#define I²C_DATA_CMD_FIRST_DATA_BYTE (0x1U << 11) /*!< Indicates the first data byte received after the address phase
for receive transfer in Master receiver or Slave receiver mode */
Listing 7. Fragment pliku mg32f10x.h zawierający definicje komend interfejsu I²C
Pod poszczególnymi makrami kryją się bowiem pojedyncze bity, ale przesunięte o 8, 9, 10 lub 11 miejsc w lewo. Parametr wywołania funkcji I2C_WriteDataCmd() jest 16-bitowy – dane umieszczone w 8 najmłodszych bitach są zatem interpretowane jako bajt do wysłania, zaś gdy interfejs znajdzie ustawiony jeden z bitów 8...11, to zostanie wykonana odpowiednia komenda. Takie rozwiązanie okazuje się szczególnie wygodne podczas odbierania wielu bajtów – po dokonaniu ostatniego odczytu wystarczy maskę I2C_DATA_CMD_READ połączyć z I2C_DATA_CMD_STOP, z czego korzystamy zresztą w kolejnej linii kodu po wywołaniu funkcji I2C_ReadData(). Jak widać na listingu 6, po wywołaniach komend odczytu następują okresy oczekiwania, realizowane metodą standardową (najprostszą) – tj. jako polling w pętli while(). Rzecz jasna takie rozwiązanie ma ograniczoną stosowalność i w praktyce powinno zostać zastąpione obsługą za pomocą przerwań, a najlepiej także DMA – zainteresowanych Czytelników zachęcamy do zapoznania się z materiałami producenta, które zawierają przykładowe programy korzystające z różnych scenariuszy użycia interfejsu I²C (a także wszystkich pozostałych peryferiów mikrokontrolera).
Po wykonaniu wszystkich opisanych powyżej operacji następuje jeszcze kilka testów. Gdy maszyna stanów mastera zakończy pracę (o czym świadczy wyzerowanie flagi I2C_FLAG_MST_ACTIVITY), warto sprawdzić, czy nie wystąpiły jakieś błędy nadawania – w tym celu odpytujemy flagę I2C_IT_TX_ABRT i (w razie jej ustawienia) pobieramy informację o przyczynie błędu, korzystając z funkcji I2C_GetTxAbortSource(). Lista potencjalnych błędów jest naprawdę długa – inżynierowie marki Megawin stanęli na wysokości zadania i przewidzieli wiele scenariuszy, które opisano na stronach 402 i 403 dokumentacji mikrokontrolera. Niezależnie od przyczyny ewentualnego błędu, po odczycie informacji diagnostycznych należy wyzerować flagę I2C_IT_TX_ABRT. Drugi blok else if {} pozwala na obsłużenie flagi odpowiadającej za zatrzaśnięcie linii SCL w stanie niskim (flaga I2C_IT_SCL_STUCK_AT_LOW).
Ostatnie operacje zamieszczone w ciele funkcji mcp9808_read_temp() mają za zadanie wyłączyć blok I²C oraz przeliczyć wartość wyjściową czujnika temperatury na odczyt wyrażony w stopniach Celsjusza (a dokładniej w jego setnych częściach, co znakomicie ułatwia późniejsze przesłanie wyniku do wyświetlacza LED). Dla uproszczenia zastosowano funkcję przeliczenia wyniku poprawną jedynie przy temperaturach nieujemnych, gdyż w gruncie rzeczy mało prawdopodobne, by płytka ewaluacyjna miała w trakcie nauki programowania znaleźć się w warunkach arktycznych – jeżeli jednak miałoby się tak zdarzyć, zainteresowanych Czytelników odsyłamy do noty katalogowej czujnika MCP9808, w której na stronie 25 znajduje się stosowny wzór odpowiedni do temperatur ujemnych.
Podsumowanie
W kolejnym odcinku naszego kursu omówiliśmy w telegraficznym skrócie obsługę przetwornika ADC, przerwań od timera SysTick oraz interfejsu I²C. Za miesiąc, w ostatniej już części cyklu, przyjrzymy się m.in. interfejsowi UART oraz wbudowanemu w mikrokontroler zegarowi czasu rzeczywistego (RTC).
inż. Przemysław Musz, EP