projekti: GPS-vastaanotin ja NMEA:n parsinta AVR:llä

[size=140]GPS-vastaanotin ja NMEA:n parsinta AVR:llä[/size]

Kirjoittelenpa nyt nopeasti omasta GPS-vastaanottimestani, jonka tein reilu vuosi sitten. Tämä artikkeli liittyy siis pitkälti Eero:n aiemmin julkaisemaan artikkeliin GPS ja Arduino.

[size=140]Projektin lähtökohdat[/size]
Reilu vuosi sitten Partcossa oli tarjouksessa Fastrax:n GPS-moduuleja, omani maksoivat 10€/kpl. (Näkyy olevan tällä hetkelläkin kympillä samantapaisia, mutta ei ihan samaa mallia). Omat moduulini ovat 4800 BAUD:n CMOS-tasoiset, malliltaan muistaakseni Fastrax uPatch100-C4. Halusin rakentaa kyseiselle moduulille AVR-pohjalta alustan, jossa sitten olisi 4x20 merkin LCD-näyttö, muutama nappula kontrollointiin ja liitäntä kyseiselle GPS-moduulille. Koska GPS on luonteeltaan varsin liikuteltava vekotin, kaavailin jännitesyötöksi 1-2 AA-paristoa step-up hakkurilla.

[size=140]Raudan speksit[/size]
Koska kyseinen GPS-moduuli toimii 3,0…3,6V käyttöjännitteellä ja se vaatii kohtuu paljon virtaa (labrapowerini mukaan tällä hetkellä n. 70mA koko laitteelle), päätin toteuttaa laitteen niin, että nostan step-up-hakkurilla paristolta jännitteen vain 3,3 volttiin, joka syötetään suoraan AVR:lle ja GPS-moduulille. Näin ei tuhlattaisi tehoa nostamalla jännite ensin viiteen volttiin tuolla 70mA+ virralla ja sitten pudotettaisi siitä 3,3 volttiin GPS-moduulille, jolloin tehoa hukkaantuisi 1,7V * 70mA = paljon :wink: Lisäksi GPS-moduulin tehonsyöttö on mahdollista katkaista PNP-tyyppisellä transistorilla AVR:ltä käsin, pariston säästötarkoituksessa, mikäli kyseistä toimintoa ikinä tarvittaisiin…
Levyllä on lisukkeina pari lediä, joista toinen on suoraan kiinni GPS-moduulin 1pps-lähdössä, eli se vilkkuu puolen hertsin taajuudella. Toinen on kiinni AVR:ssä, ja tällä hetkellä se vaihtaa tilaansa jokaisen parsitun viestin jälkeen.

[size=140]Rakentelu[/size]
Valitettavasti käytössäni olevat LCD-näytöt vaativat 5 voltin käyttöjännitteen ainakin kontrastille, joten piirtelin levylle hihasta ravistellun yksinkertaisen varauspumpun, jolla laskin LCD:n kontrastijännitteen alle järjestelmän nollatason, jolloin kontrastijännitteeksi käyttikseen nähden tuli viiden voltin luokkaa. Piirtelin ja syövyttelin laitteelle piirilevyn ja kasauksen jälkeen kävi ilmi, että eihän se tietenkään toimi oikein. Koska tuosta on jo yli vuosi aikaa en enää muista mitenkä onnistuin protoilun ryssimään, mutta jokatapauksessa, jouduin ottamaan tuon varauspumpun irti (poistamalla pari juotettua vastusta levyltä) ja tein LCD:n varsinaiselle käyttöjännitteelle varauspumpun, jolla nostan itse käyttiksen viiden voltin tietämille. Toisinsanoen levyn ohessa roikkuu piuhojen päässä killumassa toinen pieni levy, jolla tuo uusi varauspumppu on… eli toteutuksesta tuli ruma kuin rullatuoli :neutral_face: Nooh, ainakin se nyt toimii.


charge_pump.png


Tuo levy on ollut tarkoitus piirrellä ja tehdä uusiksi jossain välissä jos olisi aikaa ja motivaatiota. En ole päättänyt mitenkä tekisin nuo tehonsyötöt uudessa versiossa. Helpoin lienee nostaa step-up-hakkurilla jännite vaan kiltisti viiteen volttiin ja tiputtaa siitä yksin GPS-moduulille lineaariregulla ja signaalilinjoissa jännitejaolla, tai sitten syöttää viiden voltin käyttis vain LCD:lle ja tiputtaa GPS-moduulin ja AVR:n yhteinen käyttis 3,3 volttiin. Tietysti kahdella hakkurilla nuo tasot voisi myös tehdä ilman suuria tehohäviöitä, mutta komponenttimäärä ja -hinta kasvaa. Liekö helpoin olisi hommata vaikka samplena kaupallinen varauspumppupiiri tai vastaava ja tehdä step-up hakkurilla se 3,3V ja tehdä varauspumppupiirillä 3,3V:sta LCD:lle oikeasti jollain tapaa stabiili 5V. (eli ollaan takaisin lähtötilanteessa, korvaten vain varauspumppu paremmalla(?)…)

[size=140]Koodaus[/size]
Tästä projektista ehkä se muille hyödyllisin osa voisi olla tuo NMEA-protokollan parsinta. Siihenkin varmaan löytyisi tuhat ja yksi valmista projektia googlella, mutta itselläni on kaikissa harrasteprojekteissa aina lähtökohtana, että erityisesti koodipuolella tehdään itse niin paljon kuin mahdollista. Se on vaan niin paljon palkitsevampaa kun tietää täysin miten jokainen osa-alue softasta toimii ja on toteutettu.

Kaivoin ensin Fastraxin datalehdistä vähän tietoa kyseisestä moduulista. Kyseinen 4800 BAUD malli ei ensinnäkään ehdi sylkeä ulos kaikkia viestityyppejä, mikäli tulostusväli on sekunti. Datalehden mukaan 4800 BAUD malli ehtii tulostaa alle 6 erilaista viestiä, ja se tukee yhteensä kahdeksaa eri viestityyppiä. Halutut viestit voidaan pyytää @NC-komennolla. Datalehden mukaan se on muotoa ‘@NC dddddddd’, jossa d:t ovat integer-arvoja, jotka kukin ketovat yhdentyyppisen viestin halutun lähetysvälin. Väli voi olla 0, 1, 2 tai 5 sekuntia, jossa 0 tarkoittaa, että kyseistä viestiä ei lähetetä ollenkaan. Ne ovat järjestyksessä: GGA, GLL, GSA, GSV, RMC, VTG, ZDA, PSGSA.

Seuraavaksi kirjoittelin datalehdestä tekstitiedostoon niiden viestityyppien dataformaatin, joissa oli itseäni kiinnostavaa tietoa.

[code]GPGGA:
$GPGGA,hhmmss,AAmm.mmmm,[N|S],OOmm.mmmm[E|W],[0|1|2],00,01.2,mmmmm.m,M,039.2,M,04,0000*42

1: GGA header ($GPGGA)
2: UTC of position (hhmmss)
3: Latitude (AA: latitude in degrees, mm.mmmm: minutes of latitude)
4: North/South (N or S)
5: Longitude (OO: longitude in degrees, mm.mmmm: minutes of longitude)
6: East/West (E or W)
7: GPS Quality Indicator (0: disabled, 1: GPS positioning, 2: D-GPS positioning)
8: Number of satellites (00 to 12, number of satellites used in positioning calculation)
9: HDOP (00.0)
10: Altitude (12345.6 altitude in meters, five integer digits, one decimal digit)
11: Altitude unit (M, meters)
12: Geoidal separation (123.4, meters)
13: Geoidal separation unit (M)
14: Age of D-GPS data (00, seconds, time elapsed since D-GPS reception)
15: D-GPS reference station ID (0000)
16: Checksum (*00)

GPGSA:
$GPGSA,[A|M],[1|2|3],(00){0,12},01.6,01.0,01.3*00

1: GSA header ($GPGSA)
2: Mode (M: Manual, A: Automatic)
3: Positioning mode (1: Fix not available, 2: 2D, 3: 3D)
4…:Satellite ID number (05, this field is repeated for every satellite used in solution, max 12)
5: PDOP (01.6)
6: HDOP (01.0)
7: VDOP (01.3)
8: Checksum (*00)

GPGSV:
$GPGSV,1,1,00,00,61,056,35,…,14,52,321,47*70

1: GSV header ($GPGSV)
2: Total number of sentences (1)
3: Sentence number (1)
4: Total number of satellites in view (08)
5: Satellite ID number (05, 01 to 32)
6: Elevation (61, degrees, Elevation angle of satellite as seen from receiver, 00 to 90)
7: Azimuth (056, degrees, Satellite azimuth as seen from receiver (000 to 359)
8: SNR (C/N) (35, dBHz, Received signal level C/N, 00 to 99)
… (repeat)
xx: Checksum (*70)

GPRMC:
$GPRMC,hhmmss,[A|V],AAmm.mmmm,[N|S],OOmm.mmmm[E|W],123.4,090.7,ddmmyy,[A|D|N]*76

1: RMC header ($GPRMC)
2: UTC of position (hhmmss)
3: Status (A: Data valid, V: Data invalid)
4: Latitude (AA: latitude in degrees, mm.mmmm: minutes of latitude)
5: North/South (N or S: North Latitude or South Latitude)
6: Longitude (OO: longitude in degrees, mm.mmmm: minutes of longitude)
7: East/West (E or W: East Longitude or West Longitude)
8: Speed over ground (123.4: receiver’s speed in knots)
9: Course over ground (090.7: Receiver’s direction of travel. Moving clockwise starting at due north)
10: Date (ddmmyy)
11: Magnetic variation (not output)
12: East/West (E or W, not output)
13: Mode indicator (A: Autonomous, D: D-GPS, N: Data not valid)
14: Checksum (*76)

GPVTG:
$GPVTG,275.6,T,M,000.0,N,000.0,K,[A|D|N]*0B

1: VTG header ($GPVTG)
2: Course over ground (275.6: Receiver’s direction of travel. Moving clockwise starting at due north)
3: T
4: Course over ground (magnetic) (not output)
5. M
6: Speed over ground (123.4: receiver’s speed in knots)
7: N (unit, N: knots)
8: Speed over ground (123.4: receiver’s speed in km/h)
9: K (unit, K: km/h)
10: Mode indicator (A: Autonomous, D: D-GPS, N: Data not valid)
11: Checksum (*76)[/code]

Tuon pohjalta kirjoittelin sitten itse parseriosan, joka tulkitsee moduulin lähettämät viestit ja kaivaa niistä tiedot ulos erillisiin muuttujiinsa, joista ne voidaan sitten tulostaa LCD-näytölle.

Koodi on ATmega88:lle, ja se on toistaiseksi rumasti yhdessä tiedostossa, poislukien LCD-kirjasto.
Timer0 on PWM:ää ja siispä LCD:n varauspumppua varten.

[code]#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>
#include “lcd.h”

#define F_CPU 1000000UL
#include <util/delay.h>

#define PARSER_STATE_WAIT_HEADER 0x00
#define PARSER_STATE_WAIT_END 0x01

uint8_t flag_second = 0;
uint8_t RxRd = 0;
uint8_t RxData = 0;
volatile uint8_t RxWr = 0;
volatile uint8_t RxBuffer[256];
uint8_t TxBuffer[256];
uint8_t TxWr = 0;
volatile uint8_t TxRd = 0;

struct data_s
{
uint8_t latitude;
uint8_t latitude_min;
uint16_t latitude_min_frac;
uint8_t latitude_dir;
uint8_t longitude;
uint8_t longitude_min;
uint16_t longitude_min_frac;
uint8_t longitude_dir;

uint8_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t min;
uint8_t sec;

uint16_t altitude;
uint8_t altitude_frac;

uint8_t geoidal_separation;
uint8_t geoidal_separation_frac;
uint8_t gps_quality_indicator;
uint8_t num_sat;
uint8_t age_dgps_data;

uint8_t positioning_mode;
uint8_t num_sat_in_view;

uint16_t course;
uint16_t course_frac;
uint8_t speed_kmh;
uint8_t speed_kmh_frac;
uint8_t speed_knots;
uint8_t speed_knots_frac;

} gps_data;

uint8_t data_field_seek(volatile uint8_t *buf, uint8_t start, uint8_t tgt)
{
uint8_t i;
uint8_t cnt = 0;
uint8_t fld = 0;

i = start;

while(fld < tgt)
{
	cnt++;

	if(buf[i++] == ',')
		fld++;
}

return start + cnt;	// returns the array index to the data field start position

}

uint16_t read_int(volatile uint8_t *buf, uint8_t i, uint8_t digits)
{
uint8_t d = 0;
uint16_t value = 0;

while(d++ < digits)
{
	if(buf[i] >= 0x30 && buf[i] <= 0x39)	// 0..9
	{
		value *= 10;
		value += (buf[i++] - 0x30);
	}
	else
		break;
}

return value;

}

uint16_t read_hex(volatile uint8_t *buf, uint8_t i, uint8_t digits)
{
uint8_t d = 0;
uint16_t value = 0;

while(d++ < digits)
{
	if(buf[i] >= 0x30 && buf[i] <= 0x39)	// 0..9
	{
		value *= 16;
		value += (buf[i++] - 0x30);
	}
	else if(buf[i] >= 0x41 && buf[i] <= 0x46)	// A..F
	{
		value *= 16;
		value += (buf[i++] - 0x41 + 10);
	}
	else if(buf[i] >= 0x61 && buf[i] <= 0x66)	// a..f
	{
		value *= 16;
		value += (buf[i++] - 0x61 + 10);
	}
	else
		break;
}

return value;

}

uint8_t read_header_type(volatile uint8_t *buf, uint8_t i)
{
uint8_t xor = 0;

while(buf[i] != ',')
{
	xor = xor ^ buf[i++];
}

return xor;

}

void data_parser(uint8_t h, volatile uint8_t *buf, uint8_t start)
{
uint8_t pos;
uint32_t tmp, tmp2;

if(h == 0x72)		// GPGGA
{
	// UTC
	pos = data_field_seek(buf, start, 1);

	if(buf[pos] != ',')
	{
		gps_data.hour	= read_int(buf, pos + 0, 2);
		gps_data.min	= read_int(buf, pos + 2, 2);
		gps_data.sec	= read_int(buf, pos + 4, 2);
	}

	// Latitude
	pos = data_field_seek(buf, start, 2);

	if(buf[pos] != ',')
	{
		gps_data.latitude		= read_int(buf, pos + 0, 2);
		gps_data.latitude_min		= read_int(buf, pos + 2, 2);
		gps_data.latitude_min_frac	= read_int(buf, pos + 5, 4);	// 2 + 2 + decimal dot precedes

		if(buf[pos + 10] == 'N' || buf[pos + 10] == 'S')
			gps_data.latitude_dir = buf[pos + 10];
	}

	// Longitude
	pos = data_field_seek(buf, start, 4);

	if(buf[pos] != ',')
	{
		gps_data.longitude		= read_int(buf, pos + 0, 3);
		gps_data.longitude_min		= read_int(buf, pos + 3, 2);
		gps_data.longitude_min_frac	= read_int(buf, pos + 6, 4);	// 2 + 2 + decimal dot precedes

		if(buf[pos + 11] == 'E' || buf[pos + 11] == 'W')
			gps_data.longitude_dir = buf[pos + 11];
	}

	// Altitude
	pos = data_field_seek(buf, start, 9);

	if(buf[pos] != ',')
	{
		gps_data.altitude		= read_int(buf, pos + 0, 5);
		gps_data.altitude_frac		= read_int(buf, pos + 6, 1);	// 5 digits + decimal dot precedes
	}

	// Geoidal separation
	pos = data_field_seek(buf, start, 11);

	if(buf[pos] != ',')
	{
		gps_data.geoidal_separation		= read_int(buf, pos + 0, 3);
		gps_data.geoidal_separation_frac	= read_int(buf, pos + 4, 1);	// 3 digits + decimal dot precedes
	}
}
else if(h == 0x66)	// GPGSA
{
	// Positioning mode
	pos = data_field_seek(buf, start, 2);

	if(buf[pos] != ',')
		gps_data.positioning_mode = read_int(buf, pos, 1);

}
else if(h == 0x71)	// GPGSV
{
	// Number of satellites in view
	pos = data_field_seek(buf, start, 3);

	if(buf[pos] != ',')
		gps_data.num_sat_in_view = read_int(buf, pos, 2);

}
else if(h == 0x6F)	// GPRMC
{
	// Speed in knots
	pos = data_field_seek(buf, start, 7);

	if(buf[pos] != ',')
	{
		gps_data.speed_knots		= read_int(buf, pos + 0, 3);
		gps_data.speed_knots_frac	= read_int(buf, pos + 4, 1);	// 3 digits + decimal dot precedes

		tmp = gps_data.speed_knots;
		tmp = tmp * 18520;
		tmp2 = gps_data.speed_knots_frac;
		tmp2 = tmp2 * 1852;
		tmp = tmp + tmp2;

		gps_data.speed_kmh = tmp / 10000;
		gps_data.speed_kmh_frac = (tmp / 1000) % 10;
	}

	// Course
	pos = data_field_seek(buf, start, 8);

	if(buf[pos] != ',')
	{
		gps_data.course		= read_int(buf, pos + 0, 3);
		gps_data.course_frac	= read_int(buf, pos + 4, 1);	// 3 digits + decimal dot precedes
	}

	flag_second = 1;
}

/*
else if(h == 0x76) // GPVTG
{
flag_second = 1;
}
*/
}

void RxHandler(void)
{
uint8_t byte;
static uint8_t state = 0;
static uint8_t start = 0;
static uint8_t end = 0;

while(RxRd != RxWr)
{
	byte = RxBuffer[RxRd];

	if(state == PARSER_STATE_WAIT_HEADER)
	{
		if(byte == '$')
		{
			start = RxRd;
			state = PARSER_STATE_WAIT_END;
		}

		RxRd++;
		return;
	}
	else if(state == PARSER_STATE_WAIT_END)
	{
		if(byte == 0x0a)	// \n (LF)
		{
			uint8_t j;
			uint8_t c;
			uint8_t xor = 0;

			end = RxRd;

			j = start + 1;

			while(j != (uint8_t)(end - 4))
			{
				xor = xor ^ RxBuffer[j++];
			}

			c = read_hex(RxBuffer, (uint8_t)(end - 3), 2);

			if(c == xor)	// checksum OK
			{
				uint8_t header;
				header = read_header_type(RxBuffer, start);
				data_parser(header, RxBuffer, start);

				PIND = (1 << PD4);	// toggle yellow LED to indicate valid message
			}
			else	// checksum error
			{
			}

			state = PARSER_STATE_WAIT_HEADER;
		}

		RxRd++;
	}
}

}

/*
±-------------------+
|123^ 12.3456N sat:00| latitude; total number of satellites in view
|123^ 12.3456E 123.4m| longitude; geoidal separation
|12345.6m 11:50:32 3D| altitude, UTC, postioning mode: NA/2D/3D
|123.4km/h 123.4^ | speed, course,
±-------------------+
*/

void print_uint(uint16_t value, uint8_t chars, uint8_t zero)
{
char p_buffer[10];
uint8_t i = 0;

if(chars == 6)
	p_buffer[i++] = 0x20;

if(value >= 10000)
{
	p_buffer[i++] = (value / 10000) + 0x30;
}
else if(chars >= 5)
{
	if(zero) p_buffer[i++] = 0x30;		// "0" = zero
	else p_buffer[i++] = 0x20;
}

if(value >= 1000)
{
	p_buffer[i++] = ((value / 1000) % 10) + 0x30;
}
else if(chars >= 4)
{
	if(zero) p_buffer[i++] = 0x30;
	else p_buffer[i++] = 0x20;
}

if(value >= 100)
{
	p_buffer[i++] = ((value / 100) % 10) + 0x30;
}
else if(chars >= 3)
{
	if(zero) p_buffer[i++] = 0x30;
	else p_buffer[i++] = 0x20;
}

if(value >= 10)
{
	p_buffer[i++] = ((value / 10) % 10) + 0x30;
}
else if(chars >= 2)
{
	if(zero) p_buffer[i++] = 0x30;
	else p_buffer[i++] = 0x20;
}

p_buffer[i++] = (value % 10) + 0x30;
p_buffer[i] = '\0';

lcd_puts(p_buffer);

}

/*
0x7E: arrow right
0x7F: arrow left

0xDF: degree sign
0xE0: alfa
0xE1: ä
0xE2: beta
0xE3: epsilon
0xE4: my
0xE6: gamma
0xEB: asterisk
0xEF: ö
0xF2: theta
0xF3: infinity
0xF4: ohm
0xF6: sum
0xF7: pi
*/

void init_gps_screen(void)
{
lcd_gotoxy(0, 0);
lcd_puts_p(PSTR(" \xDF . sat:"));
lcd_gotoxy(0, 1);
lcd_puts_p(PSTR(" \xDF . . m"));
lcd_gotoxy(0, 2);
lcd_puts_p(PSTR(" . m : :"));
lcd_gotoxy(0, 3);
lcd_puts_p(PSTR(" . km/h . \xDF"));
}

void print_gps(void)
{
lcd_gotoxy(0, 0);
print_uint(gps_data.latitude, 3, 0);

lcd_gotoxy(5, 0);
print_uint(gps_data.latitude_min, 2, 0);

lcd_gotoxy(8, 0);
print_uint(gps_data.latitude_min_frac, 4, 1);

if(gps_data.latitude_dir)
	lcd_putc(gps_data.latitude_dir);

lcd_gotoxy(18, 0);
print_uint(gps_data.num_sat_in_view, 2, 1);

lcd_gotoxy(0, 1);
print_uint(gps_data.longitude, 3, 0);

lcd_gotoxy(5, 1);
print_uint(gps_data.longitude_min, 2, 0);

lcd_gotoxy(8, 1);
print_uint(gps_data.longitude_min_frac, 4, 1);

if(gps_data.longitude_dir)
	lcd_putc(gps_data.longitude_dir);

lcd_gotoxy(14, 1);
print_uint(gps_data.geoidal_separation, 3, 0);

lcd_gotoxy(18, 1);
print_uint(gps_data.geoidal_separation_frac, 1, 1);


lcd_gotoxy(0, 2);
print_uint(gps_data.altitude, 5, 0);

lcd_gotoxy(6, 2);
print_uint(gps_data.altitude_frac, 1, 1);

lcd_gotoxy(9, 2);
print_uint(gps_data.hour, 2, 1);

lcd_gotoxy(12, 2);
print_uint(gps_data.min, 2, 1);

lcd_gotoxy(15, 2);
print_uint(gps_data.sec, 2, 1);

lcd_gotoxy(18, 2);

if(gps_data.positioning_mode == 0)
	lcd_puts_p(PSTR("NA"));
else if(gps_data.positioning_mode == 1)
	lcd_puts_p(PSTR("2D"));
else if(gps_data.positioning_mode == 2)
	lcd_puts_p(PSTR("3D"));

lcd_gotoxy(0, 3);
print_uint(gps_data.speed_kmh, 3, 0);

lcd_gotoxy(4, 3);
print_uint(gps_data.speed_kmh_frac, 1, 1);

lcd_gotoxy(10, 3);
print_uint(gps_data.course, 3, 0);

lcd_gotoxy(14, 3);
print_uint(gps_data.course_frac, 1, 1);

}

ISR(USART_UDRE_vect)
{
if(TxRd != TxWr)
UDR0 = TxBuffer[TxRd++]; // Send one byte of data

else
	UCSR0B &= ~(1 << UDRIE0);		// No more data, disable interrupt

}

ISR(USART_RX_vect)
{
RxBuffer[RxWr++] = UDR0;
}

int main(void)
{
DDRB = 0x3F; // PB0…5 outputs: LCD RS; LCD E; LCD D4…D7
DDRD = 0xB4; // PD2, PD4, PD5, PD7 outputs: GPS power enable; LED3; OC0B output for LCD Vcont charge pump; LCD BL enable
PORTC = 0x3F; // PC0…5 pullups: Enable pullups for buttons
PORTD = 0x80; // PD7 high to enable LCD BL FIXME: button press activates timer; PD2 low to enable GPS FIXME button combination to toggle

UBRR0L = 12;						// 4800 BAUD @ 1 MHz
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);			// 8 data bits
UCSR0B = (1 << RXCIE0) | (1 << RXEN0);			// Rx Complete Interrupt Enable, Receiver Enable
UCSR0B |= (1 << TXEN0);

OCR0A = 0x80;						// OCR0A = TOP: ~3937 Hz PWM freq @ 1 MHz F_CPU, ps 1, OCR0A = 0x80
OCR0B = 0x40;
TCCR0A = (1 << COM0B1) | (1 << WGM00);			// Clear OC0B when up-counting, set when down-counting; Select Phase correct PWM mode, TOP = OCR0A
TCCR0B = (1 << WGM02);					// Select Phase correct PWM mode, TOP = OCR0A (WGM02 = 1, WGM00 = 1)
TCCR0B |= (1 << CS00);					// Enable Timer/Counter0 with ps 1

lcd_init(LCD_DISP_ON);
lcd_clrscr();

init_gps_screen();

sei();

while(1)
{
	RxHandler();

	if(flag_second)
	{
		print_gps();
		flag_second = 0;
	}
}

return 0;

}[/code]

Perusidea parserissani on seuraava (jälleen koodia lunttaamalla, kun en muista suoraan mitään…):

  • data_field_seek()-funktiolla etsitään viestistä tietyn datatietueen alkamiskohta. Tämä siksi, että osa tietueista voi olla tyhjiä (tai niiden pituus voi vaihdella(?) tai sitten ei…), joten niiden paikka merkkijonossa vaihtelee.
  • read_int() funktiolla voidaan lukea merkkijonosta integer-tyyppinen muuttuja. Funktio ottaa sisäänsä merkkijonotaulukon osoitteen, halutun tiedon alkamiskohdan eli indeksin taulukossa, sekä tiedon/arvon maksimipituuden.
  • read_hex()-funktio samalla idealla kuin read_int()
  • read_header_type()-funktio on varmaan vähän hämärä, sen tarkoituksena on nyt lukea viestin tyyppi, jotta osataan parsia kukin viestityyppi oikealla parserillaan. Se generoi viestin tyypiksi yhden 8-bittisen muuttujan XOR:aamalla viestin otsikkokentän merkit keskenään, josta nyt ainakin tässä tapauksessa tulee joka viestille eri arvo. Joten tuo ei ole mikään geneerinen ja välttämättä suositeltava toteutustapa. Tällä siis vältetään vain strcmp()-kutsu eli merkkijonovertailu, joka tuntui vielä hivenen bloatimmalta toteutukselta.
  • data_parser()-funktio tekee varsinaisen parsinnan. Se on jaettu if-lausein lohkoiksi, joista yksi lohko parsii yhdentyyppiset viestit. Viestien parsinta menee niin, että sen mukaan mitä tietoa kussakin viestissä on, päätin mitkä niistä ovat mielenkiintoisia itselleni, ja sitten laskin monennessako tietueessa eli pilkulla erotetussa kentässä kyseinen tieto on. Sitten data_field_seek()-funktiolla etsin kyseisen tietueen alkamiskohdan, ja sitten luen siitä halutun tiedon joko read_int() tai read_hex()-funtiolla, sen mukaan minkä tyyppistä tietoa kentässä on.
  • RxHandler()-funktion tehtävänä on lukea moduulin lähettämä tieto, etsiä siitä viestien alkukohdat eli ‘$’-merkit ja lukea siitä eteenpäin yksi viesti tietoa, laskea viestistä tarkistussumma (XOR) ja mikäli tarkistussumma täsmää, niin viesti parsitaan ja sitten siirrytään odottamaan uutta viestiä. Meni äsken hetki ihmetellessä miten parseri hoitaa bufferin lopun ympäri wrappauksen, mutta se onkin tehty käyttämällä 256-alkioista bufferia ja 8-bittisiä indeksejä, niin homma toimii ihan itsekseen. Aika mustaa magiaa… :wink:

Noiden lisäksi onkin sitten vain vähän LCD:lle tulosteluja ja nopeuksien muunnoksia mph -> km/h yms pientä. Näköjään nappeja ei tällä hetkellä lueta lainkaan.

Heitin kaikki koodit yhteen pakettiin, jos joku haluaa kokeilla suoraan. Eli kunhan UART-pinnit on samat (ATmega88:n UART0), kontrollerin kellotaajuus on sama 1 MHz ja LED:ien ja PWM:n lähtöpinneissä ei ole mitään muuta sotkemassa, niin pitäisi toimia suoraan. Aseta LCD:n pinnit lcd_hw.h headerissa. LCD:n datapinnit (4-bit moodissa) ovat nyt PB5…2. Jos käytät muuta pinnialuetta kuin 5…2, niin joudut muuttamaan lcd.c:stä lcd_out_low() ja lcd_out_high() funktioita sekä lcd_data_port_out() makroa vastaamaan pinnejäsi. Tämä siksi, että poistin aikoinaan alkuperäisestä lcd-kirjastosta datapinnien yksittäisen bit-bangaamisen kun olen aina käyttänyt LCD:n dataväylää peräkkäisissä pinneissä, jolloin koodin koko aavistuksen pienenee. Pitäisi tuokin toteutus joskus siivota.

gps_2011-04-01.zip (8.56 KB)

Eli tältä näyttää lopputulos:

Kuvassa kaikki näyttää nollaa, koska tuo GPS-moduuli (ja aika useat GPS:t muutenkin) vaatii vahvan signaalin, eli toimii ainoastaan ulkona. Toisinsanoen kuvassa se ei ole saanut satelliiteilta mitään dataa, joten kaikki kentät ovat tyhjiä, joten muuttujien arvoja ei ole päivitetty kertaakaan.

Olen vermeen kanssa hyppinyt ulkona testailemassa ja kyllä se toimii. Huomaa paikan muutoksen jopa n. 30cm:n tarkkuudella tjsp. Ja esim. autossa näyttää aika hyvin oikeaa nopeutta (tai siis samaa kuin mittari, jollain pienellä offsetillä, johtuen auton mittarista nimenomaan, olettaisin ainakin…) ja samoin liikkumissuunta näkyy oikein (asteina, lähtien pohjoisesta myötäpäivään). Kellonajan laite hakee myös satelliitilta, näkyy muistaakseni UTC-aikana, eli pitäisi laskea tunteihin offset aikavyöhykkeen mukaan.

[size=140]Jatkokehitys[/size]
Tarkoituksena oli lisätä laitteeseen koordinaattien tallennus esim SD-muistikortille, sekä sijaintien tallennus nimellä ja sitten kahden sijainnin välisen matkan laskenta linnuntietä. Tuo laskenta vaan tyssäsi siihen kun ihmettelin pikaisesti wikipediasta ja googlesta millähän kaavoilla se matka laskettaisiinkaan tälläisen litteän maapallon tapauksessa kaarevaa pintaa pitkin. Ei ole tuo matikka valitettavasti niin hyvin hallussa tätä nykyä :neutral_face: Näiden ominaisuuksien lisäksi sitten tosiaan edellä mainittu piirilevyn uudelleentekeminen, jotta saadaan ylimääräiset killuttimet pois ja laite siistiin pakettiin.

[size=130]Muutoshistoria[/size]
2011-03-31 23:14: Ensimmäinen versio uunista ulos
2011-04-01 13:12: Lisätty koodit zip-paketissa ja hiukan kommenttia koodista/pinniasetuksista

Todella hienoa! Kiitos kun jaoit tämän.

Itselläni on tuo Partcossa nyt myynnissä oleve GPS moduli. Sen kanssa on ollut pientä murhetta kun sitä täytyy välillä herätellä arduinon resetin jäljiltä, irrottamalla VBAT, mutta kunhan se saa taivallisen signaalin, mitä suotta murehtimaan, se ottaa vain tovin.

Täytyy kokeilla koodiasi ja laittaa anturi tosikoetukselle, tähän asti se on vain mitannut aikaa ikkunan välissä.

Kari