; GPS reader routine - Parses & Stores only vital data ; This program will communicate with any NMEA-0183 V2.0 compatible GPS receiver ; operating at 4800 baud, 8-N-1, and capable of transmitting GGA and RMC ; sentences. ; ; It will look for a particular sentence (ie $GPRMC) and display selected sentence ; information on the LCD display. ; ; Information displayed is minutes N latitude, minutes W longitude, Speed in tenths of knots, heading in degrees ; ; Version 1.0 (works!) ; Written by Kevin Nichols, 12-1-00, 4:09pm for CSE 466 ; ; Port Usage ; P3.0 (RXD) = Serial input data from GPS unit ; NOTE! Serial data from the GPS unit is INVERTED from ; what the 89C55 wants, so it must be run through an inverter first! ; ; Uses Timer2 in mode 1 as the baud rate generator ; For 32.00 MHz crystal, use 65328 as value for RCAP2H,RCAP2L to ; Get a baud rate of 4808 (close enough) ; ; KNOWN ISSUES: 1)If no GPS reading is being sent (ie if the unit loses sat lock) ; The values in raw_xxx will be garbage (sorry!) Could put in a routine to ; Check if GPS unit is OK (ie "A", not "V") before putting any data in. Ahh future improvements... ; 2) Get a garbage character displayed at end of line 2. Assuming its a display deal... ; ; ; ;------------------------------------------------------------------------------ $NOMOD51 ; disable predefined 8051 registers $INCLUDE (At89c55.INC) ; include CPU definition file for Atmel 89C55 ;------------------------------------------------------------------------------ ; Module name ;------------------------------------------------------------------------------ NAME Test_GPS_Driver ;------------------------------------------------------------------------------ ; Segment and variable declarations ;------------------------------------------------------------------------------ ; ;------------------------------------------------------------------------------ ; Put the STACK segment in the main module. ;------------------------------------------------------------------------------ STACK1 SEGMENT IDATA ; STACK1 goes into IDATA RAM. RSEG STACK1 ; switch to STACK1 segment. DS 24 ; reserve 24 bytes stack space ;------------------------------------------------------------------------------ ; DATA SEGMENT--Reserves space in DATA RAM (lower 128 bytes)-- ;------------------------------------------------------------------------------ VARS SEGMENT DATA ; segment for DATA RAM. RSEG VARS ; switch to this data segment code_buf_ptr: DS 1 ; Stores the address of the next free code buffer location. char_buf_ptr: DS 1 ; Stores the address of the next free character buffer location. code_buf_cntr: DS 1 ; Stores the maximum number of bytes that can be put in the code_buffer char_buf_cntr: DS 1 ; Stores the maximum number of bytes that can be put in the char_buffer parse_key_cntr: DS 1 ; Stores how far we are into the parse_key table (0 to n) index: DS 1 ; Holds current index into char string ;------------------------------------------------------------------------------ ; IDATA SEGMENT--Reserves space in INDIRECT DATA RAM (upper 128 bytes)-- ;------------------------------------------------------------------------------ VARS2 SEGMENT IDATA ; segment for DATA RAM. RSEG VARS2 ; switch to this data segment code_buffer: DS 6 ; Contains the first 6 chars from each sentence received, ; So that we can save on RAM by being pickey about which sentences we store. char_buffer: DS 80 ; Our 80 character buffer for raw ASCII GPS data. Remember to change init function if this size changes! raw_latitude: DS 5 ; North latitude. Ex: 4751.698 (47 deg, 51.698 minutes) would be stored as 51698 raw_longitude: DS 5 ; West longitude. Ex: 12209.774 (122 deg, 09.774 minutes) would be stored as 09774 raw_speed: DS 3 ; In tenths of knots. Ex: 312.7 knots would be stored as 127 raw_heading: DS 3 ; Heading w/r to True North. Ex: 237.8 degrees would be stored as 237 ;------------------------------------------------------------------------------ ; BIT SEGMENT--Reserves space in BIT RAM ;------------------------------------------------------------------------------ bit_flags SEGMENT BIT ; segment for BIT RAM. RSEG bit_flags ; switch to this bit segment good_s: DBIT 1 ; Set means we're in the process of receiving a sentence that we want s_avail: DBIT 1 ; Set means a complete sentence is now available in the buffer code_buff: DBIT 1 ; Set means characters should be stored in the code buffer. ignore: DBIT 1 ; Set means we want to ignore characters (we still check for "$" though) overflow: DBIT 1 ; Set means that the char buffer has overflowed ;--------------------------------------------------------------------------- ; ROM Lookup Table ;--------------------------------------------------------------------------- CONST SEGMENT CODE RSEG CONST sentence: DB '$GPRMC' ; Put the sentence here that we're looking for ; The parse key is used to parse the string into the characters you are interested in ; Values: A comma "," means to skip characters until you get to a comma, then advance to next char ; An "S" means to skip that single character, & advance to next char ; An "R" means to read that character into the ram location the current pointer is pointing at ; An "E" means we're done parsing (return from subroutine.) ; NOTE: The RAM locations to be filled MUST BE CONTIGUOUS, and in the proper order (ie the first ; raw_xxxx that goes with the first bunch of R's must be DS'd first, then the second, etc.) ; Obviously, the number of chars to be stored must match the amount of RAM DS'd for each variable exactly. parse_key: DB ',,,SSRRSRRR,,SSSRRSRRR,,SRRSR,RRRE' ;------------------------------------------------------------------------------ ; CONSTANTS (typeless) ;------------------------------------------------------------------------------ e_line EQU P0_0 ; Port P0.0 Enable line (1 = enable) rs EQU P0_1 ; Port P0.1 RS line (1 = data, 0 = instructions) lcd_data EQU P1 ; Port P1 (P1.4 - P1.7) are LCD data lines ;------------------------------------------------------------------------------ ; LJMP to "start" ;------------------------------------------------------------------------------ CSEG AT 0 ; absolute Segment at Address 0 LJMP start ; reset location (jump to start) ;------------------------------------------------------------------------------ ; CODE SEGMENT--Reserves space in CODE ROM for assembler instructions. ;------------------------------------------------------------------------------ code_seg_name SEGMENT CODE RSEG code_seg_name ; switch to this code segment USING 0 ; Use register bank 0 start: MOV SP,#STACK1-1 ; assign stack at beginning CALL init ; Initialize the LCD Display & the serial I/O pause: NOP JNB s_avail,pause ; If the sentence isn't good yet, then just waste time CALL parse_me ; Parse the string into the raw_xxx variables CALL show_em ; Update the LCD display CLR s_avail ; Reset the flag (since we've already done what we need to with this sentence) CLR overflow ; Clear out the overflow flag (just in case) JMP pause ; Go back & wait for a sentence update ;------------------------------------------------------------------------------ ; Serial Port Interrupt Service Routine ; ;------------------------------------------------------------------------------ CSEG AT 23H ; 23H is address for Serial Port interrupt LJMP serial_isr ;------------------------------------------------------------------------------ ; ;------------------------------------------------------------------------------ ; This routine looks at the chars received from the serial port. ; If we get a "$", we put it, and the next 5 chars in code_buffer. Once all 6 chars are in there, ; we check them to see if they represent the sentence we want. If so, we copy them to the ; char_buffer, and keep putting future chars in there until we get another "$", at which point we ; set the gps_update flag, and start putting chars in the code buffer again. If they don't represent ; the sentence we want, we "flush the buffer" and ignore everything until we get a "$" char. ; ; Note: Immediately after start-up, we ignore all chars until we get a "$" to start the whole process. ; serial_isr_seg SEGMENT CODE ; segment for interrupt function RSEG serial_isr_seg ; switch to this code segment USING 2 ; register bank for interrupt routine serial_isr: PUSH PSW MOV PSW,#10H ; Use register bank 2 PUSH ACC MOV A,SBUF ; Get the character CJNE A,#'$',no_start_chr ; Did we get a "$"? ; Yes, we did get a "$" JNB good_s,load_buffer ; Is the good_s bit set? CLR good_s ; Yes, the good_s bit is set, so clear it SETB s_avail ; and set the sentence available flag MOV R0,char_buf_ptr ; Get current value of the char pointer MOV @R0,#0x00 ; Put in the end flag "0" for the LCD display load_buffer: MOV R0,#code_buffer ; Get the address of code_buffer (to access indirect RAM) MOV @R0,A ; Put the "$" into position 0 of the code buffer (indirect RAM) MOV code_buf_ptr,#code_buffer+1 ;Initialize code_buf_ptr to next avail open space MOV code_buf_cntr,#0x01 ; Counter starts out at 1 (since just put "$" into position 0 SETB code_buff ; We want future characters to go into the code buffer CLR ignore ; We would like to receive characters SJMP return ; Return from interrupt no_start_chr: JB ignore,return ; If ignore bit is set, just return from interrups JB code_buff,load_code ; If code bit is set, put it in the code buffer ; Code_buff flag not set (so put in char buffer) MOV R1,char_buf_cntr ; Get the number of chars we've gotten so far CJNE R1,#0x5A,load_char ; If buffer not full (ie = 90), load char buffer ; Buffer is full! SETB ignore SETB overflow SJMP return load_char: MOV R0,char_buf_ptr ; Get current value of the char pointer MOV @R0,A ; Save char in the buffer INC char_buf_ptr INC char_buf_cntr SJMP return load_code: MOV R0,code_buf_ptr ; Get location of where to store character MOV @R0,A ; Store it in the code buffer INC code_buf_ptr ; Point to the next open spot INC code_buf_cntr ; Add one to the count MOV R1,code_buf_cntr CJNE R1,#0x06,return ; If code buffer isn't filled, just return ; Code buffer is full... MOV R0,#code_buffer ; Get a pointer to the start of the code buffer MOV code_buf_cntr,#0x00 ; Initialize counter loop_1: MOV A,@R0 ; Get char from code buffer MOV R1,A ; Put it into R1 MOV DPTR,#sentence ; Point to start of ASCII message table (the sentence we're looking for) MOV A,code_buf_cntr ; Get current index into table MOVC A,@A+DPTR ; Get character from ROM into A CLR C ; Don't want the carry to interfere (ie subtract w/borrow) SUBB A,R1 ; Compare the two characters JNZ bad_sentence ; If different, jump MOV A,code_buf_cntr ; Grab a copy of the counter value CLR C ; Dont want carry to interfere SUBB A,#0x05 ; compare with 5 JZ good_sentence ; Have we compared all 6 (0 thru 5) characters? If so, its a good sentence INC R0 ; If not, compare more INC code_buf_cntr SJMP loop_1 bad_sentence: SETB ignore ; A character was different CLR good_s SJMP return good_sentence: SETB good_s MOV char_buf_cntr,#0x00 ; Initialize the char buffer counter MOV char_buf_ptr,#char_buffer ; Point to next open slot (won't contain the $GPXXX) CLR code_buff ; Now we want to save characters in the character buffer return: CLR RI ; Clear the interrupt bit POP ACC POP PSW RETI ;------------------------------------------------------------------------------ ; SUBROUTINES ;------------------------------------------------------------------------------ sub_segment SEGMENT CODE ; segment for interrupt function RSEG sub_segment ; switch to this code segment USING 1 ; register bank for subroutines; ;-------------------------------------------------- ; INITIALIZATION - Used to initialize all stuff on chip after reset ;-------------------------------------------------- ;---------------------------------------------- ; First, init the LCD stuff ;---------------------------------------------- init: CLR e_line CLR rs CLR P1_4 CLR P1_5 CLR P1_6 CLR P1_7 ; Initialize the LCD Display ; Wait for 15ms MOV R1,#150 ; Set delay time CALL var_delay ; Send init command MOV lcd_data,#0x30 SETB e_line NOP ; Min cycle time is 500ns CLR e_line ; Wait for 4.1ms MOV R1,#41 CALL var_delay ; Send init command MOV lcd_data,#0x30 SETB e_line NOP ; Min cycle time is 500ns CLR e_line ; Wait for 100us MOV R1,1 CALL var_delay ; Send init command MOV A,#0x30 CALL lcd_write ; Send function set command ; 4-bit bus, 2 rows, 5x7 dots MOV A,#0x20 CALL lcd_write MOV A,#0x20 CALL lcd_write MOV A,#0x80 CALL lcd_write ; Send display control command ; Display on, cursor off, no blinking MOV A,#0x00 CALL lcd_write MOV A,#0xC0 CALL lcd_write ; Send clear display command ; Clear display, Cursor address = 0 MOV A,#0x00 CALL lcd_write MOV R1,#16 CALL var_delay MOV A,#0x10 CALL lcd_write MOV R1,#16 CALL var_delay ; Send entry mode command ; Increment, no display shift MOV A,0x00 CALL lcd_write MOV A,#0x60 CALL lcd_write ; Need to store a "0" end flag at the first location of char_buffer, so that if the LCD ; routine tries to display a char before anything has been put in the buffer, it ; will just end immediately. MOV R0,#char_buffer ;Point to the first location MOV @R0,#0x00 ;Store the "0" end flag there. ;---------------------------------------------- ; Second, init the serial port ;---------------------------------------------- MOV RCAP2H,#0xFF MOV RCAP2L,#0x30 ; Set up timer for 4800 baud rate MOV code_buf_ptr,#code_buffer ; Initialize pointer to first slot in code_buffer MOV char_buf_ptr,#char_buffer ; Initialize pointer to first slot in char_buffer MOV code_buf_cntr,#0x00 ; Counts number of characters in code buffer MOV char_buf_cntr,#0x00 ; Counts number of characters in caracter buffer SETB ignore CLR good_s MOV T2MOD,#0x02 ; Set up T2MOD to enable timer 2 output MOV T2CON,#0x34 ; Set up timer 2 and start it. (RCLK for T2, TCKL for T2, turn on T2) MOV IE,#0x90 ; Disable timer 2 interrupts, enable serial port interrupts MOV SCON,#0x70 ; Set up serial control register, & enable it. RET ; From the initialization routine ;var_delay routine. ;-------------------------- ; To use: Load variable R1 with number of tenths of milliseconds you want to delay, ; then call this subroutine. Routine will return after specified amount of time. ; Min. delay time (R1 = 1) is 100us, max. delay time (R1 = 0) is 25.6ms. ; Assumes a 32MHz crystal, for an internal cycle time of 375ns (internal divide-by-12) ; Formula is delay = R1 * 100us (if R1 = 1, get a 100us delay, if R1 = 10, get a 1ms delay) ; The timing is close, but not exact. See following chart: ; R1 = 1, Delay Error = +0.875us (100.875us) ; R1 = 10, Delay Error = +1.25us (1001.25us) ; R1 = 100, Delay Error = +13.3us (10.0133ms) ; R1 = 0, Delay Error = +32.75us (25.63275ms) ; Number of cycles for each instruction shown in comments var_delay: MOV R0,#132 ;1 = 375ns L1: DJNZ R0,L1 ;2 = 750ns (do this 132 times) DJNZ R1,var_delay ;2 = 750ns (do this R1 times) RET ;2 = 750ns ;lcd_write routine - Sends data to the LCD lcd_write: MOV lcd_data,A ; Upper 4 bits connect to LCD SETB e_line ; Toggle enable line NOP ; 500ns minimum cycle time CLR e_line MOV R0,#53 ;Simple 40us delay loop for LCD L2: DJNZ R0,L2 ; RET ;lcd_addr - Routine to send LCD address for start of characters lcd_addr: CLR rs ; Put LCD in command mode MOV lcd_data,A SETB e_line ; Clock in the data NOP ; 500ns minimum cycle time CLR e_line MOV R0,#53 ;Simple 40us delay loop for LCD L4: DJNZ R0,L4 ; SETB rs ; Put LCD in data mode RET ;Update the LCD display... show_em: ; Send clear display command ; Clear display, Cursor address = 0 MOV A,#0x00 CALL lcd_write MOV R1,#16 CALL var_delay MOV A,#0x10 CALL lcd_write MOV R1,#16 CALL var_delay ; Set cursor to firsl line MOV A,#80H ; Addr = 80H MSB (remember, we send 4 bits at a time!) CALL lcd_addr ; (See Optrex spec sheet for address format) MOV A,#00H ; Addr = 80H LSB CALL lcd_addr ;Do the Latitude first MOV R5,#0x02 ; Show two characters MOV R1,#raw_latitude ; Point to start to show the first two chars CALL show_chrs MOV A,#'.' MOV R5,#0x01 CALL show_a_char MOV R5,#0x03 MOV R1,#raw_latitude + 2 CALL show_chrs MOV A,#'L' MOV R5,#0x01 CALL show_a_char MOV A,#'a' MOV R5,#0x01 CALL show_a_char MOV A,#'t' MOV R5,#0x01 CALL show_a_char MOV A,#' ' MOV R5,#0x01 CALL show_a_char ;Do Longitude next MOV R5,#0x02 ; Show two characters MOV R1,#raw_longitude ; Point to start to show the first two chars CALL show_chrs MOV A,#'.' MOV R5,#0x01 CALL show_a_char MOV R5,#0x03 MOV R1,#raw_longitude + 2 CALL show_chrs MOV A,#'L' MOV R5,#0x01 CALL show_a_char MOV A,#'o' MOV R5,#0x01 CALL show_a_char MOV A,#'n' MOV R5,#0x01 CALL show_a_char ;Set address to second line MOV A,#0xC0 ; Addr = C0H MSB (remember, we send 4 bits at a time!) CALL lcd_addr ; (See Optrex spec sheet for address format) MOV A,#0x00 ; Addr = C0H LSB CALL lcd_addr MOV A,#'S' MOV R5,#0x01 CALL show_a_char MOV A,#'p' MOV R5,#0x01 CALL show_a_char MOV A,#'e' MOV R5,#0x01 CALL show_a_char MOV A,#'e' MOV R5,#0x01 CALL show_a_char MOV A,#'d' MOV R5,#0x01 CALL show_a_char MOV A,#' ' MOV R5,#0x01 CALL show_a_char MOV R5,#0x02 ; Show two characters MOV R1,#raw_speed ; Point to start to show the first two chars CALL show_chrs MOV A,#'.' MOV R5,#0x01 CALL show_a_char MOV R5,#0x01 ; Show two characters MOV R1,#raw_speed + 2 ; Point to start to show the first two chars CALL show_chrs MOV A,#' ' MOV R5,#0x01 CALL show_a_char MOV A,#' ' MOV R5,#0x01 CALL show_a_char MOV A,#'H' MOV R5,#0x01 CALL show_a_char MOV A,#'d' MOV R5,#0x01 CALL show_a_char MOV A,#'n' MOV R5,#0x01 CALL show_a_char MOV A,#'g' MOV R5,#0x01 CALL show_a_char MOV A,#' ' MOV R5,#0x01 CALL show_a_char MOV R5,#0x03 ; Show three characters MOV R1,#raw_heading ; Point to start to show the first two chars CALL show_chrs NOP RET ; On "show_chars" entry, R5 should contain the number of characters to display, ; and R1 should contain the address of the start of the characters to display. ; On "show_a_char" entry, A should contain character to dosplay, & R5 must contain 0x01 ; show_chrs: MOV A,@R1 show_a_char: CALL lcd_write ANL A,#0x0F ; Mask high nibble, so rotate works like shift RL A RL A RL A RL A CALL lcd_write DJNZ R5,incr_char_ptr RET incr_char_ptr: INC R1 SJMP show_chrs ;----------------------------------------------------------------------------- ; Subroutine to parse the sentence into the vars we want ;----------------------------------------------------------------------------- parse_me: ;initialize pointers here MOV R0,#char_buffer ; Get pointer to the start of character buffer MOV R1,#raw_latitude ; Start with pointer to raw_latitude MOV parse_key_cntr,#0x00 ; Initialize the counter parse_loop: ; Get the next char from parse_key that we're looking for into R2 MOV DPTR,#parse_key ; Point to start of parse_key table MOV A,parse_key_cntr ; Get current index into table MOVC A,@A+DPTR ; Get character from parse_key into A MOV R2,A ; Transfer into R2 & do assy version of a CASE statement... ;Case "," CJNE R2,#',',try1 ; Do "skip to next comma" routine here MOV A,@R0 ; Get character from char buffer nxt_char2: CJNE A,#',',nxt_char1 ; Is it a comma? INC R0 ; If so, then point to next char & INC parse_key_cntr ; increment parse key counter & SJMP parse_loop ; loop back to top nxt_char1: INC R0 ; Point to next character in char buffer MOV A,@R0 ; Get next character from char buffer SJMP nxt_char2 ;Case "S" try1: CJNE R2,#'S',try2 ; Do skip single character routine here INC R0 ; Increment char buffer pointer INC parse_key_cntr ; Advance to next parse key char SJMP parse_loop ;Case "R" try2: CJNE R2,#'R',try3 ; Do record character here MOV A,@R0 ; Get character from char buffer MOV @R1,A ; Put the character in the next open slot INC parse_key_cntr ; Point to next parse_key char INC R0 ; Increment char buffer pointer INC R1 ; Point to next slot in raw_xxxx variable SJMP parse_loop ;Case "E" try3: CJNE R2,#'E',try_bad ; Do end routine here RET ;Just return ; try_bad: ;Do what needs to be done if we read a bad character from the parse_key table RET ; For now, just return ;------------------------------------------------------------------------------ ; The END directive is ALWAYS required. ;------------------------------------------------------------------------------ END ; End Of File