Kompas cyfrowy na STM32 i MAG3110

Kompas cyfrowy na STM32 i MAG3110

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

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 (dokumnetacja do pobrania po 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 (yymax) 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

div#stuning-header .dfd-stuning-header-bg-container {background-size: initial;background-position: top center;background-attachment: initial;background-repeat: initial;}#stuning-header div.page-title-inner {min-height: 650px;}