风筝
发表于: 2018-10-4 09:35:12 | 显示全部楼层

本篇文章主要描述了如何通过I2C总线连接一个简单的GPS模块。使用的控制器是ATtiny841:

i2cgps.jpg


简介

将GPS纳入到项目中是一项非常艰巨的任务。首先,您必须正确解析您使用的GPS模块返回的NMEA语句,然后如果您使用接收到的经度和纬度进行任何计算,则需要将浮点GPS库结合到例程中来执行计算。


如果您需要处理一些任何其他重要的事情,GPS处理任务可能会干扰您的其他任务。将GPS处理作为单独的I2C模块解决了这个问题。


我最初设计这个是为运行我的Lisp解释器uLisp的电路板提供GPS支持,但它对于您希望通过简单的I2C接口访问GPS数据的任何其他应用程序都很有用。


GPS变量

I2C数据中的18个字节包含从NMEA消息中提取的原始GPS数据:

i2cgpscodes.gif


这些变量如下:

偏移量
变量
说明
0
Time
以HH:MM格式表示的时间。
2
Csecs
以厘秒为单位的秒数。
4
Lat
以1e-4弧分为单位的纬度。
8
Long
以1e-4弧分为单位的经度。
12
Knots
速度,单位1e-2节。
14
Course
轨道,单位1e-2度。
16
Date
DD:MM格式的日期。

角度测量、纬度和经度以1e-4弧分为单位返回。因此,一度表示为600,000单位。这旨在允许使用长整数来完成算术,并且非常适合解析GPS模块返回的值而不会丢失任何精度。

◾    如果您喜欢使用百万分之一度的纬度和经度,就像其他一些GPS库一样,只需乘以5/3即可。

◾    如果要将纬度和经度用作浮点数(以度为单位),请将它们除以6e5。

◾    如果您想使用纬度和经度作为度、弧分和弧秒,请使用下面演示程序中的计算。


电路

我决定将电路基于ATtiny841控制器,因为它在硬件中提供USART和从I2C功能:

i2cgpsmodule.gif


为方便起见,我使用了Adafruit Ultimate GPS Breakout,但该电路适用于其他GPS模块。


由于ATtiny841仅采用SOIC封装,我将其安装在分线板上。 GPS模块和分线板都整齐地安装在迷你面包板上,可从SparkFun获得。


我使用了一个晶振时钟,虽然您可以使用内部8MHz振荡器,只要您使用OSCCAL寄存器进行校准即可。我在晶体和Vcc之间连接了18pF电容,而不是传统的GND,以使原型板上的布线更容易。这不应该影响电路的运行。


根据您连接I2C GPS模块的情况,您可能需要在SDA和SCL线路与Vcc之间使用4.7kΩ上拉电阻。我发现我将模块连接到Arduino Uno时不需要他们,但使用Aduino Zero则确实需要他们。


程序代码

首先,我们定义一个typedef,它定义了一个18字节的buffer_t类型,也可以通过GPS变量名称或字节数组来引用:

  1. // GPS variables
  2. typedef union {
  3.   struct {
  4.     unsigned int Time, Csecs;
  5.     long Lat, Long;
  6.     unsigned int Knots, Course, Date;
  7.   };
  8.   uint8_t Data[buffersize];
  9. } buffer_t;
复制代码

该程序的工作原理如下。当从GPS模块接收到每个字符时,由ParseGPS()函数解析,该函数将GPS变量写入gps.Data []缓冲区,直到Fix指示已收到完整的NMEA句子:

  1.   do {
  2.     while (!Serial.available());
  3.     char c = Serial.read();
  4.     ParseGPS(c);
  5.   } while(!Fix);
复制代码

I2C接口产生TWI中断,对于通过I2C接口写入或读取的每个字节调用TWI_SLAVE中断。 I2C接口直接从gps.Data []缓冲区读取数据是不安全的。想象一下,当我们在中断时通过I2S访问缓冲区时,是否从GPS模块读取GPS数据。当我们读取LSB时,双字节时间可能是11:59。但是,当我们读取MSB时,它可能已更改为12:00。然后我们将以12:59的时间结束,这将是完全错误的。任何其他字段都可能出现同样的问题。


为防止这种情况,我们使用两个缓冲GPS数据由ParseGPS()写入gps.Data [],当我们收到完整的NMEA消息时,会在一次操作中将其复制到buf.Data [],并禁用中断。


如果我们通过I2C直接从buf.Data []读取,可能会发生同样的问题。在读取两个连续字节之间可以更新数据。解决方案是在I2C例程开始时将buf.Data []复制到第三个缓冲区i2c.Data []。然后我们从这个安全数据中读取数据,直到它是一致的。


另一种解决方案是让一个过程等到另一个过程完成。但是,使用单独的缓冲区是一种更优雅的解决方案,可确保数据始终保持一致而不会占用程序。


该电路提供可选的INT输出,当接收到完整的NMEA语句以指示新数据准备好时,该输出为低电平。当I2C读取数据时,输出再次变为高电平。


TWI中断服务程序

我决定自己实现从属软件,而不是使用库,目的是使项目尽可能简单。以下处理I2C接口的代码:

  1. ISR(TWI_SLAVE_vect) {
  2.   if (TWSSRA & 1<<TWASIF) {                // Address received
  3.     if (TWSSRA & 1<<TWDIR) {               // Master read
  4.       // Copy buf buffer to i2c buffer so it's ready to be read
  5.       for (int i=0; i<buffersize; i++) i2c.Data[i] = buf.Data[i];
  6.       // Take INT pin high
  7.       digitalWrite(Interrupt, HIGH);
  8.       TWSCRB = 3<<TWCMD0;                  // ACK
  9.     } else {                               // Master write
  10.       TWSCRB = 3<<TWCMD0;                  // ACK
  11.     }
  12.   } else if (TWSSRA & 1<<TWDIF) {          // Data interrupt
  13.     if (TWSSRA & 1<<TWDIR) {               // Master read
  14.       if (Position >= 0 && Position < buffersize) {
  15.         TWSD = i2c.Data[Position++];
  16.         TWSCRB = 3<<TWCMD0;
  17.       } else {
  18.         TWSCRB = 2<<TWCMD0;                // NAK and complete
  19.       }
  20.     } else {                               // Master write
  21.       Position = TWSD;                     // Write sets position
  22.       TWSCRB = 3<<TWCMD0;                  // ACK
  23.     }
  24.   }
  25. }
复制代码

这个例程的逻辑如下:

◾    如果中断是针对地址的:

      ◾    如果是一次读取:复制将由i2c读取的GPS数据,以及ACK

      ◾    如果是一次写入:只是ACK。

◾    如果中断是针对数据的:

      ◾    如果是一次读取:从位置的缓冲区读取数据。

      ◾    如果是一次写入:将缓冲区指针Position设置为byte。


每个I2C会话都以写入开始,以指定缓冲区中的偏移量以读取后续变量。然后,您可以读取任意数量的字节以获取要访问的变量。


编译程序

我使用Spence Konde的新ATTiny Core编译了该程序,该核心现在支持所有ATtiny处理器并取代了早期的各种ATtiny内核。在Boards菜单上的ATtiny Universal标题下选择ATtinyx41选项。然后后续菜单选择B.O.D. Disabled, ATtiny841, 8 MHz (external)


通过合适的编程器将电路连接到计算机,例如Tiny AVR Programmer Board,然后从Programmer菜单中选择编程器。请注意,您必须选择标记为(ATtiny)的编程器才能正确上传到大多数支持的芯片 - 这显然是由于IDE的限制。


选择Burn Bootloader以适当设置保险丝。然后选择Upload将程序上传到ATtiny841。


这是整个I2C GPS模块程序: I2C GPS模块程序.rar (1.72 KB, 下载次数: 11)


使用LCD字符显示器测试I2C GPS模块

为了测试模块,我编写了一个简单的程序,在LCD字符显示器上显示纬度和经度,电路如下:

i2cgpslcd.jpg


以下例程读取I2C GPS模块返回的长整数值并将其打印为度、分和秒:

  1. void printDMS (long ang) {
  2.   int degs, mins, secs;
  3.   degs = abs(ang)/600000; mins = (abs(ang) - degs*600000)/10000;
  4.   secs = (abs(ang) - (long)degs*600000 - (long)mins*10000)*6/1000;
  5.   lcd.print(degs); lcd.print((char)0xdf); lcd.print(mins);
  6.   lcd.print('\''); lcd.print(secs); lcd.print('"');
  7. }
复制代码

我很高兴地发现LCD字符显示器可以显示度数符号;它的字符是0xdf。


最后,这是从I2C GPS模块获取经度和纬度并将其写入LCD显示器的例程:

  1. void loop() {
  2.   Wire.beginTransmission(0x3A);
  3.   Wire.write(4);                                 // Start with latitude
  4.   Wire.endTransmission();
  5.   Wire.requestFrom(0x3A, 8);                     // Ask for 8 bytes
  6.   long Lat = 0, Long = 0;
  7.   for (int i=0; i<4; i++) Lat = Lat | (long)Wire.read()<<(i*8);
  8.   for (int i=0; i<4; i++) Long = Long | (long)Wire.read()<<(i*8);
  9.   lcd.cmd(0x01);
  10.   lcd.print(" Lat: "); printDMS(Lat); lcd.print((Lat < 0) ? 'S' : 'N');
  11.   lcd.cmd(0xc0);                                 // Clear
  12.   lcd.print("Long: "); printDMS(Long); lcd.print((Long < 0) ? 'W' : 'E');
  13.   while (digitalRead(Ready));                    // Wait for Ready to go low
  14. }
复制代码

使用uLisp测试I2C GPS模块

我还使用在Arduino Zero上运行的uLisp解释器测试了I2C显示模块。 这是用于读取和打印七个GPS值的uLisp程序:

  1. (defun read2 (str)
  2.   (+ (read-byte str) (ash (read-byte str) 8)))

  3. (defun read4 (str)
  4.   (+ (read-byte str) (ash (read-byte str) 8)
  5.      (ash (read-byte str) 16) (ash (read-byte str) 24)))

  6. (defun rd ()
  7.   (with-i2c (str 58)
  8.     (write-byte 0 str)
  9.     (restart-i2c str 18)
  10.     (princ "Time:") (princ (read2 str))
  11.     (princ ", Sec:") (princ (/ (read2 str) 100))
  12.     (princ ", Lat:") (princ (/ (read4 str) 6e5))
  13.     (princ ", Long:") (princ (/ (read4 str) 6e5))
  14.     (princ ", Knot:") (princ (/ (read2 str) 100))
  15.     (princ ", Course:") (princ (/ (read2 str) 100))
  16.     (princ ", Date:") (princ (read2 str)) (terpri)))

  17. (defun go ()
  18.   (loop (rd) (delay 1000)))
复制代码

键入(go)每秒打印一行值; 例如:

  1. Time:1043, Sec:48, Lat:52.2187, Long:0.137323, Knot:0.09, Course:332.24, Date:2709
复制代码

参考链接

◾    Adafruit的Ultimate GPS Breakout

◾    Proto-PIC的Ultimate GPS Breakout

◾    用于SOIC-14或TSSOP-14的SMT Breakout PCB

◾    GitHub上的ATTinyCore

跳转到指定楼层
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

主题 32 | 回复: 41



手机版|

GMT+8, 2024-5-7 01:15 , Processed in 0.047064 second(s), 6 queries , Gzip On, MemCache On. Powered by Discuz! X3.5

YiBoard一板网 © 2015-2022 地址:河北省石家庄市长安区高营大街 ( 冀ICP备18020117号 )

快速回复 返回顶部 返回列表