W przykładzie przedstawiamy realizację kompasu cyfrowego bazującego na magnetometrze MAG3110. Dodatkową funkcją aplikacji jest cyfrowe zobrazowanie temperatury odczytanej z wewnętrznego czujnika magnetometru. Projekt wykonano na zestawie ZL27ARM, wynik pomiaru zostanie przedstawiony w postaci graficznej na kolorowym wyświetlaczu LCD.
W przykładowej aplikacji posłużymy się 3-osiowym czujnikiem pola magnetycznego MAG3110, który za pośrednictwem magistrali I2C komunikuje się z mikrokontrolerem STM32F103 z zestawu ZL27ARM. Do zestawu jest również podłączony kolorowy wyświetlacz KAmodTFT2, prezentujący dane z czujnika. Sposoby podłączenia elementów peryferyjnych do mikrokontrolera pokazano na rysunku poniżej.
Rys. 1. Schemat połączeń zestawu testowego
Schemat elektryczny modułu z sensorem MAG3110 (i alternatywnym HMC5883 firmy Honeywell) pokazano na rysunku 2, dokumentacja produkcyjna płytki drukowanej oraz artykuł prezentujący możliwości tego układu jest dostępny pod adresem.
Rys. 2. Schemat elektryczny modułu z sensorem MAG3110 (dokumentacja do pobrania po adresem)
Po konfiguracji mikrokontrolera program przygotowuje do pracy kontroler wyświetlacza w module KAmodTFT2 oraz magnetometr MAG3110. Następnie w nieskończonej pętli zrealizowana jest obsługa menu głównego, program sprawdza stan przycisków celem zmiany wybranej opcji w menu:
if (!GPIO_ReadInputDataBit(JOY_SW_PORT,JOY_UP) && (pozycja>1)){ aktualizacja=pozycja; pozycja--; } if (!GPIO_ReadInputDataBit(JOY_SW_PORT, JOY_DOWN) && (pozycja<4)){ aktualizacja=pozycja; pozycja++; }
Następnie, jeśli nie zatwierdzono wyboru przyciskiem JOY_ENTER oraz wybrana pozycja z menu się zmieniła, następuje jego przerysowanie:
//Rysowanie menu if (aktualizacja>=0) { if (aktualizacja==0) { TFTN_Clear(0x000); TFTN_FillRect( 0, 0, 131, 18, 0x555); TFTN_WriteXY("MENU\0",45,2,0xFFF,0x555, font2); } for (i=1;i<=4;i++){ if ((i==aktualizacja)||(aktualizacja==0)) { TFTN_FillRect( 0, i*18+8, 131, i*18+8+18, 0x000); TFTN_WriteXY(menu[i-1],5,i*18+10,0xFFF,0x000, font2); } if (i==pozycja) { TFTN_FillRect( 0, i*18+8, 131, i*18+8+18, 0x000); TFTN_WriteXY(menu[i-1],5,i*18+10,0xF00,0x000, font2); } } aktualizacja=-1; }
Jak widać aktualizacja wyświetlacza odbywa się tylko przy wybraniu nowej opcji z menu.
Gdy dokonamy wyboru poprzez naciśnięcie przycisku JOY_ENTER, wówczas program sprawdzi, która opcja menu była ostatnio wybrana i przejdzie do wykonania odpowiedniej funkcji:
if (!GPIO_ReadInputDataBit(JOY_PORT_ENTER, JOY_ENTER)) { switch (pozycja){ case 1: Opcja_Info(); break; case 2: Opcja_Kalibracja(); break; case 3: Opcja_Kompas(); break; case 4: Opcja_Krzywe(); break; } aktualizacja=0; }
Funkcje te wykonywane są do wciśnięcia JOY_DOWN. Pierwsza z nich to Opcja_INFO():
void Opcja_Info(void){ volatile unsigned long int i; TFTN_Clear(0x000); TFTN_WriteXY(" DEMO \0" , 5, 10,0xFFF,0x000, font2); TFTN_WriteXY(" magnetometru\0", 5, 26,0xFFF,0x000, font2); TFTN_WriteXY(" MAG3110\0" , 5, 42,0xFFF,0x000, font2); TFTN_WriteXY(" UWAGA!!!\0" , 5, 68,0xFFF,0x000, font2); TFTN_WriteXY(" Dla wiarygodnego\0", 5, 82,0xFFF,0x000, font1); TFTN_WriteXY(" pomiaru ustaw os Z\0" , 5, 90,0xFFF,0x000, font1); TFTN_WriteXY("czujnika prostopadle\0",5,98,0xFFF,0x000, font1); TFTN_WriteXY(" do ziemi oraz\0",5,106,0xFFF,0x000, font1); TFTN_WriteXY("wykonaj kalibracje!!\0",5,114,0xFFF,0x000, font1); //Czekaj na JOY_DOWN while(GPIO_ReadInputDataBit(JOY_SW_PORT, JOY_DOWN)) ; }
Ma ona za zadanie poinformować użytkownika o sposobie ustawienia i konfiguracji magnetometru, podkreślając by oś Z czujnika była ustawiona w przybliżeniu prostopadle do ziemi. Druga funkcja wybierana z opcji menu to Opcja_Kalibracja():
void Opcja_Kalibracja(void) { volatile unsigned long int i; volatile unsigned long int petla =500; //Czas kalibracji signed short x, y,z; signed short xmin=32767 ,xmax=-32768,ymin=32767,ymax=-32768; extern signed short offsetx,offsety; TFTN_Clear(0x000); TFTN_WriteXY(" KALIBRACJA \0" , 5, 10,0xFFF,0x000, font2); TFTN_WriteXY(" UWAGA!!! " , 5, 35,0xFFF,0x000, font2); TFTN_WriteXY("W czasie trwania\0" , 5, 55,0xFFF,0x000, font1); TFTN_WriteXY("sygnalu dzwiekowego\0", 5, 63,0xFFF,0x000, font1); TFTN_WriteXY("dokonaj minimalnie \0" , 5, 71,0xFFF,0x000, font1); TFTN_WriteXY("jednego obrotu \0",5,79,0xFFF,0x000, font1); TFTN_WriteXY("czujnika wzgledem osi\0",5,87,0xFFF,0x000, font1); TFTN_WriteXY("'Z' ustawionej do \0",5,95,0xFFF,0x000, font1); TFTN_WriteXY("ziemi prostopadle\0",5,103,0xFFF,0x000, font1); TFTN_WriteXY("Wcisnij JOY_ENTER\0",5,121,0xFFF,0x000, font1); //Czekaj na JOY_ENTER while(GPIO_ReadInputDataBit(JOY_PORT_ENTER, JOY_ENTER)); //Wyznaczanie minimalnych i maksymalnych wartosci pola magnetycznego na osiach X i Y //po wykonaniu co najminej jednego obrotu czujnika wokol osi Z ustawionej prostopadle //do ziemi while (petla) { MAG3110_GetPosition(&x, &y, &z); if (xxmax) xmax=x; if (y ymax) ymax=y; petla--; //W czasie kalibracji Speaker wydaje dzwiek GPIO_WriteBit(GPIOB, GPIO_Pin_12, (BitAction)..... ....(1-GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_12))); } //Przesuniecie ukladu wspolrzednych tak aby //jego srodek znajdowal sie w polowie roznicy minimalnego i maksymalnego //pola magnetycznego odczytanego przez magnetometr dla osi X oraz Y offsetx=abs((xmin+xmax)*0.5); if (abs(xmin) Funkcja cyklicznie odczytuje wartość pola magnetycznego z czujnika dla każdej osi za pomocą funkcji:
void MAG3110_GetPosition(signed short * x,signed short * y, signed short * z) { //reads 5 registers values into buffer in multiple bytes mode, buffer[0]=x, [2]=z, [3]=y unsigned char Bufor[6]={0}; unsigned int NumByteToReadN, i; //Check, if I2C is free while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); I2C_GenerateSTART(I2C1,ENABLE); //Test on EV5 and clear it while(!I2C_CheckEvent(MAG3110_I2C, I2C_EVENT_MASTER_MODE_SELECT)); //Send MAG3110 address, set I2C master in transmiter mode I2C_Send7bitAddress(MAG3110_I2C, MAG3110_Addr, I2C_Direction_Transmitter); //Test on EV6 and clear it while(!I2C_CheckEvent(MAG3110_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //Send base register address, set address autoincrement I2C_SendData(MAG3110_I2C, 0x01); //Test on EV8 and clear it while(!I2C_CheckEvent(MAG3110_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); //Re-generate START, transmition from slave beginning I2C_GenerateSTART(MAG3110_I2C,ENABLE); //Test on EV5 and clear it while(!I2C_CheckEvent(MAG3110_I2C, I2C_EVENT_MASTER_MODE_SELECT)); //Send MAG3110 address, set I2C master in receiver mode I2C_Send7bitAddress(MAG3110_I2C, MAG3110_Addr, I2C_Direction_Receiver); //Test on EV6 and clear it while(!I2C_CheckEvent(MAG3110_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); NumByteToReadN=6; //Read 6 consecutive registers i=0; //Current read count while(NumByteToReadN) { //Before receiving last byte, disable acknowledge and generate stop if(NumByteToReadN == 1) { I2C_AcknowledgeConfig(MAG3110_I2C, DISABLE); I2C_GenerateSTOP(MAG3110_I2C, ENABLE); } //Test on EV7 and clear it while(!I2C_CheckEvent(MAG3110_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED)); //Read a byte from the MAG3110 Bufor[i] = I2C_ReceiveData(MAG3110_I2C); i++; NumByteToReadN--; } //Enable Acknowledge for next transmission I2C_AcknowledgeConfig(MAG3110_I2C, ENABLE); //Assign data to axis variables *x=(signed short)((Bufor[0]<<8)|Bufor[1]); *y=(signed short)((Bufor[2]<<8)|Bufor[3]); *z=(signed short)((Bufor[4]<<8)|Bufor[5]); }po czym wyznacza wartość minimalną i maksymalną pola magnetycznego tylko dla osi X oraz Y ponieważ oś Z nie jest nam potrzebna do dalszych obliczeń. Cykl ten powtarza się przez czas określony zmienną petla. W czasie tym konieczne jest obrócenie MAG3110 co najmniej o jeden pełny obrót wokół osi Z lub więcej razy, celem odczytania wszystkich możliwych wartości danych magnetometru dla danego terenu w którym się znajduje. Gdy czas na kalibrację się skończy, zostaje obliczone przesunięcie układu współrzędnych tak, by jego środek znajdował się pomiędzy maksymalną i minimalną wartością pola magnetycznego odczytanego z czujnika dla osi X oraz Y. Funkcja po wykonaniu tego wszystkiego automatycznie powraca do menu, w którym to możemy włączyć opcję kompasu, mając pewność że poprzez kalibrację obniżyliśmy błąd obliczanego kąta użytego do wyświetlenia róży wiatrów w funkcji Opcja_Kompas():
void Opcja_Kompas(void) { volatile unsigned long int i; signed short x, y, z; float scaledx, scaledy, heading; float declinationAngle = 0.0880; float headingDegrees=0; unsigned char Tekst[4] = {"\0"}; extern signed short offsetx,offsety; volatile signed short k,srednia; volatile int l,xn=0,xs=0,yn=0,ys=0,xw=0,yw=0,xe=0,ye=0; volatile int xns,xss,yns,yss,xws,yws,xes,yes ; volatile int xo1,yo1,xo2,yo2,xo3,yo3,xo4,yo4; volatile int xo1s,yo1s,xo2s,yo2s,xo3s,yo3s,xo4s,yo4s; float stopien = (2 * 3.1415926) / 360; //Rysowanie rozy wiatrow TFTN_Clear(0x000); TFTN_WriteBMP(roza, 0, 0, 132, 132); //Kompas dziala do momentu wcisniecia JOY_DOWN while(GPIO_ReadInputDataBit(JOY_SW_PORT, JOY_DOWN)) { k=(signed short)headingDegrees; //kat opsujacy kierunek geograficzny //Stare wspolrzedne rownaja sie nowym //celem wyczyszczenia starych danych z wyswietlacza xns=xn; yns=yn; xss=xs; yss=ys; xws=xw; yws=yw; xes=xe; yes=ye; xo1s=xo1; yo1s=yo1; xo2s=xo2; yo2s=yo2; xo3s=xo3; yo3s=yo3; xo4s=xo4; yo4s=yo4; //Obliczanie nowych wspolrzednych dla rysowania //N,S,W,E,o,o,o,o w odniesieniu do kąta kierunku geograficznego xn = (int)(62 + 59 * sin(stopien*k)); yn = (int)(60 + 59 * cos(stopien*k)); xo1 = (int)(62 + 59 * sin(stopien*(k+45))); yo1 = (int)(60 + 59 * cos(stopien*(k+45))); xo2 = (int)(62 + 59 * sin(stopien*(k+135))); yo2 = (int)(60 + 59 * cos(stopien*(k+135))); xo3 = (int)(62 + 59 * sin(stopien*(k-45))); yo3 = (int)(60 + 59 * cos(stopien*(k-45))); xo4 = (int)(62 + 59 * sin(stopien*(k-135))); yo4 = (int)(60 + 59 * cos(stopien*(k-135))); xw = (int)(62 + 59 * sin(stopien*(k+90))); yw = (int)(60 + 59 * cos(stopien*(k+90))); xs = (int)(62 + 59 * sin(stopien*(k+180))); ys = (int)(60 + 59 * cos(stopien*(k+180))); xe = (int)(62 + 59 * sin(stopien*(k-90))); ye = (int)(60 + 59 * cos(stopien*(k-90))); //Czyszczenie poprzednich znakow i rysowanie nowych TFTN_WriteXY(" \0" ,xns, yns,0x000,0x000, font2); TFTN_WriteXY("N\0" ,xn, yn,0xF00,0x000, font2); TFTN_WriteXY(" \0" ,xss, yss,0x000,0x000, font2); TFTN_WriteXY("S\0" ,xs, ys,0x00F,0x000, font2); TFTN_WriteXY(" \0" ,xws, yws,0x000,0x000, font2); TFTN_WriteXY("W\0" ,xw, yw,0xFFF,0x000, font2); TFTN_WriteXY(" \0" ,xes, yes,0x000,0x000, font2); TFTN_WriteXY("E\0" ,xe, ye,0xFFF,0x000, font2); TFTN_WriteXY(" \0" ,xo1s, yo1s,0x000,0x000, font1); TFTN_WriteXY("o\0" ,xo1, yo1,0xFFF,0x000, font1); TFTN_WriteXY(" \0" ,xo2s, yo2s,0x000,0x000, font1); TFTN_WriteXY("o\0" ,xo2, yo2,0xFFF,0x000, font1); TFTN_WriteXY(" \0" ,xo3s, yo3s,0x000,0x000, font1); TFTN_WriteXY("o\0" ,xo3, yo3,0xFFF,0x000, font1); TFTN_WriteXY(" \0" ,xo4s, yo4s,0x000,0x000, font1); TFTN_WriteXY("o\0" ,xo4, yo4,0xFFF,0x000, font1); //Pobranie danych pola magnetycznego z czujnika MAG3110_GetPosition(&x, &y, &z); //Odczytanie temperatury MAG3110_ReadRegister(0x0F,&temp); //Obliczenie kierunku geograficznego //Skalowanie wartosci z czujnika i przesuniecie //ukladu wspolrzednych scaledx=(x+offsetx)*m_Scale; scaledy=(y+offsety)*m_Scale; //Obliczenie kierunku geograficznego heading = atan2(scaledy, scaledx); heading += declinationAngle; if(heading < 0) heading += 6.2831853; //2 pi if(heading > 6.2831853) heading -= 6.2831853; //Zamiana wyniku na stopnie headingDegrees = heading * 180/3.1415926; //Przeksztalcenie liczby na stringa dla katu i wyswietlenie sprintf((char *)Tekst, "%3d\0", (signed short)headingDegrees); TFTN_WriteXY("Kat=" ,85, 120,0x000,0xFFF, font1); TFTN_WriteXY(Tekst ,110, 120,0x000,0xFFF, font1); //Przeksztalcenie liczby na stringa dla temperatury oraz wyswietlenie sprintf((char *)Tekst, "%3d\0", (signed char)temp+27); TFTN_WriteXY("T=",5, 120,0x000,0xFFF, font1); TFTN_WriteXY(Tekst,18, 120,0x000,0xFFF, font1); //W czasie pracy kompasu mruga LED 8 GPIO_WriteBit(GPIOB, GPIO_Pin_15, (BitAction)... ...(1-GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_15))); } }Za pomocą funkcji trygonometrycznych obliczane są współrzędne punktu końca wirtualnej linii która ma swój początek na środku ekranu. Współrzędne te zmieniają się w zależności od kąta obrazującego kierunek geograficzny, dzięki takiemu rozwiązaniu mamy w zależności od kąta ruch punktu po okręgu o określonej średnicy. Skoro zmienna „k” oznacza nam położenie południa to k+180 da nam północ itd. Wyjaśnienia wymaga zmienna declinationAngle, która jest wyznaczona za pomocą strony http://magnetic-declination.com/ oraz przekształcona na radiany (kalkulator jest dostępny pod adresem) i oznacza ona deklinację magnetyczną.
Ponieważ do realizacji kompasu elektronicznego został użyty sam magnetometr występuje prawdopodobieństwo, że kąt będzie się gwałtownie zmieniał w pewnym przedziale, a to oznacza drganie liter obrazujących kierunki świata na ekranie.
Na „odreagowanie” potencjalnych problemów została stworzona rozrywkowa funkcja rysująca losowe figury, służąca tylko i wyłącznie rozrywce i nie ma nic wspólnego z kompasem prócz tego że także wykorzystuje obliczenia trygonometryczne:
void Opcja_Krzywe(void) { float stopien = (2 * 3.1415926) / 360; unsigned short kata=0,katb=0,katc=0; unsigned char a=0,b=0,c=0; volatile int xl1,yl1,xl2,yl2,xl3,yl3; volatile signed char seta,setb,setc; unsigned int kolor; extern volatile unsigned char losowo; unsigned char Efekt[12] = {"\0"}; TFTN_Clear(0x000); //Losowe wartości kilku zmiennych (losowa pierwsza figura) seta=abs((rand()+losowo)%10+1); setb=abs((rand()+losowo)%10+1); setc=abs((rand()+losowo)%10+1); //Wyjscie do menu glownego przez JOY_DOWN while(GPIO_ReadInputDataBit(JOY_SW_PORT, JOY_DOWN)) { a++; b++; c++; if (a==seta) { xl1 = (int)(66 + 20 * sin(stopien*kata)); yl1 = (int)(66 + 20 * cos(stopien*kata)); a=0; kata++; if(kata==359) kata=0; } if(b==setb) { xl2 = (int)(xl1 + 20 * sin(stopien*katb)); yl2 = (int)(yl1 + 20 * cos(stopien*katb)); b=0; katb++; if(katb==359) katb=0; } if(c==setc) { xl3 = (int)(xl2 + 20 * sin(stopien*katc)); yl3 = (int)(yl2 + 20 * cos(stopien*katc)); c=0; katc++; if(katc==359) katc=0; } //Zmiaana koloru w zalerznosci od figury kolor=((15-seta)<<8 | ((15-setb)*10)<<4 | ((15-setc)*100)); //Ruysowanie figury TFTN_PutPixel(xl3, yl3, kolor); //Po wcisnieciu JOY_ENTER kolejny obrazek if (!GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_5)) { TFTN_Clear(0x000); //Losowa predkosc obrotu kazdej z 3 niewidocznych lini // // Linia 1 Linia 2 Linia 3 // 0.0------------xl1,yl1------------xl2,yl2------------xl3,yl3(rysowanie piksela) // seta setb setc // seta=abs((rand()+losowo)%10+1); setb=abs((rand()+losowo)%10+1); setc=abs((rand()+losowo)%10+1); a=0; b=0; c=0; //Predkosci przedstawione na LCD sprintf((char *)Efekt, "%2d %2d %2d\0", seta,setb,setc); TFTN_WriteXY(Efekt ,0, 3,0xFFF,0x000, font1); } } }Tak naprawdę to są trzy proste o jednakowej długości połączone do siebie szeregowo, z czego pierwsza z nich ma stały punkt zaczepu tj. środek ekrany a jej koniec jest początkiem następnej prostej itd. z czego ostatnia linia swoim końcem rysuje pewne krzywe, które są zobrazowane na TFT. Każda z części tej 3-odcinkowej prostej obraca się z pewną losową prędkością względem swojego początku, co daje ciekawy efekt graficzny. Dodatkowo kreślone figury zmieniają swój kolor w zależności od ich kształtu, a informacje na temat prędkości są wyświetlone na ekranie.
Marcin Maszlanka, KAMAMI.pl