|
本篇文章主要描述了如何通过I2C总线连接一个简单的GPS模块。使用的控制器是ATtiny841:
简介 将GPS纳入到项目中是一项非常艰巨的任务。首先,您必须正确解析您使用的GPS模块返回的NMEA语句,然后如果您使用接收到的经度和纬度进行任何计算,则需要将浮点GPS库结合到例程中来执行计算。
如果您需要处理一些任何其他重要的事情,GPS处理任务可能会干扰您的其他任务。将GPS处理作为单独的I2C模块解决了这个问题。
我最初设计这个是为运行我的Lisp解释器uLisp的电路板提供GPS支持,但它对于您希望通过简单的I2C接口访问GPS数据的任何其他应用程序都很有用。
GPS变量 I2C数据中的18个字节包含从NMEA消息中提取的原始GPS数据:
这些变量如下: 偏移量 | 变量 | 说明 | 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功能:
为方便起见,我使用了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变量名称或字节数组来引用: - // GPS variables
- typedef union {
- struct {
- unsigned int Time, Csecs;
- long Lat, Long;
- unsigned int Knots, Course, Date;
- };
- uint8_t Data[buffersize];
- } buffer_t;
复制代码
该程序的工作原理如下。当从GPS模块接收到每个字符时,由ParseGPS()函数解析,该函数将GPS变量写入gps.Data []缓冲区,直到Fix指示已收到完整的NMEA句子: - do {
- while (!Serial.available());
- char c = Serial.read();
- ParseGPS(c);
- } 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接口的代码: - ISR(TWI_SLAVE_vect) {
- if (TWSSRA & 1<<TWASIF) { // Address received
- if (TWSSRA & 1<<TWDIR) { // Master read
- // Copy buf buffer to i2c buffer so it's ready to be read
- for (int i=0; i<buffersize; i++) i2c.Data[i] = buf.Data[i];
- // Take INT pin high
- digitalWrite(Interrupt, HIGH);
- TWSCRB = 3<<TWCMD0; // ACK
- } else { // Master write
- TWSCRB = 3<<TWCMD0; // ACK
- }
- } else if (TWSSRA & 1<<TWDIF) { // Data interrupt
- if (TWSSRA & 1<<TWDIR) { // Master read
- if (Position >= 0 && Position < buffersize) {
- TWSD = i2c.Data[Position++];
- TWSCRB = 3<<TWCMD0;
- } else {
- TWSCRB = 2<<TWCMD0; // NAK and complete
- }
- } else { // Master write
- Position = TWSD; // Write sets position
- TWSCRB = 3<<TWCMD0; // ACK
- }
- }
- }
复制代码
这个例程的逻辑如下:
◾ 如果中断是针对地址的: ◾ 如果是一次读取:复制将由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字符显示器上显示纬度和经度,电路如下:
以下例程读取I2C GPS模块返回的长整数值并将其打印为度、分和秒: - void printDMS (long ang) {
- int degs, mins, secs;
- degs = abs(ang)/600000; mins = (abs(ang) - degs*600000)/10000;
- secs = (abs(ang) - (long)degs*600000 - (long)mins*10000)*6/1000;
- lcd.print(degs); lcd.print((char)0xdf); lcd.print(mins);
- lcd.print('\''); lcd.print(secs); lcd.print('"');
- }
复制代码
我很高兴地发现LCD字符显示器可以显示度数符号;它的字符是0xdf。
最后,这是从I2C GPS模块获取经度和纬度并将其写入LCD显示器的例程: - void loop() {
- Wire.beginTransmission(0x3A);
- Wire.write(4); // Start with latitude
- Wire.endTransmission();
- Wire.requestFrom(0x3A, 8); // Ask for 8 bytes
- long Lat = 0, Long = 0;
- for (int i=0; i<4; i++) Lat = Lat | (long)Wire.read()<<(i*8);
- for (int i=0; i<4; i++) Long = Long | (long)Wire.read()<<(i*8);
- lcd.cmd(0x01);
- lcd.print(" Lat: "); printDMS(Lat); lcd.print((Lat < 0) ? 'S' : 'N');
- lcd.cmd(0xc0); // Clear
- lcd.print("Long: "); printDMS(Long); lcd.print((Long < 0) ? 'W' : 'E');
- while (digitalRead(Ready)); // Wait for Ready to go low
- }
复制代码
使用uLisp测试I2C GPS模块 我还使用在Arduino Zero上运行的uLisp解释器测试了I2C显示模块。 这是用于读取和打印七个GPS值的uLisp程序: - (defun read2 (str)
- (+ (read-byte str) (ash (read-byte str) 8)))
- (defun read4 (str)
- (+ (read-byte str) (ash (read-byte str) 8)
- (ash (read-byte str) 16) (ash (read-byte str) 24)))
- (defun rd ()
- (with-i2c (str 58)
- (write-byte 0 str)
- (restart-i2c str 18)
- (princ "Time:") (princ (read2 str))
- (princ ", Sec:") (princ (/ (read2 str) 100))
- (princ ", Lat:") (princ (/ (read4 str) 6e5))
- (princ ", Long:") (princ (/ (read4 str) 6e5))
- (princ ", Knot:") (princ (/ (read2 str) 100))
- (princ ", Course:") (princ (/ (read2 str) 100))
- (princ ", Date:") (princ (read2 str)) (terpri)))
- (defun go ()
- (loop (rd) (delay 1000)))
复制代码
键入(go)每秒打印一行值; 例如: - 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。 |