ATMega16與HMC5883

在一些運動系統中,有時需要進行精確的方向控制,雖然測量方向的方法有多種,但最便利、通用性最強的還是測量地球的磁場。利用地磁作爲參考,通過傳感器測量出與地磁線之間的夾角就可以得到方位角的數據,從而實現精確的方向控制。這裏就來討論一下地磁傳感器(又稱爲數字羅盤或電子羅盤)及其使用方法。

常用的地磁傳感器主要有FreeScale(飛思卡爾)MAG系列和Honeywell(霍尼韋爾)HMC系列,下面就以市場上常見的HoneywellHMC5883的地磁傳感器來進行討論。

HMC5883是一種表面貼裝的高集成度、帶有IIC數字接口的弱磁傳感器芯片。它內含有最先進的高分辨率HMC118X系列磁阻傳感器,並附帶霍尼韋爾專利的集成電路(包括有放大器、自動消磁驅動器和偏差校準等),具有12位模數轉換器能使羅盤精度控制在1°~2°之間。霍尼韋爾的磁傳感器在低磁場傳感器行業中是靈敏度最高和可靠性最好的傳感器。其測量範圍能從毫高斯到8高斯(gauss)。下面是它的封裝外形和引腳圖。

地磁傳感器(HMC5883) - 西區故事 - 松柏後凋        地磁傳感器(HMC5883) - 西區故事 - 松柏後凋 

HMC5883的工作電壓在2.16V~3.6V之間,典型爲3.3V。雖然工作電壓爲低電壓方式,但數據端口的電壓可通過VDDIO口來指定,因此它與單片機的接口有兩種方式,一種是單片機爲5V方式,一種是單片機爲3.3V方式,具體如如下圖所示。

地磁傳感器(HMC5883) - 西區故事 - 松柏後凋地磁傳感器(HMC5883) - 西區故事 - 松柏後凋
  
上圖中左邊的是單片機工作在5V電壓的連接方式,右邊是單片機工作在3.3V電壓的連接方式。從圖中可以看出,實際上左邊的連接方式只要電壓高於1.71V都可以,出就是說對於某些工作在1.71V~2.16V電壓之間單片機也可用此種連接方式。
對於HMC5883的電氣參數及特性請參看其管方的數據手冊,這裏只討論如何應用HMC5883來獲取地磁數據。由於對模塊的控制一般都是通過寫相應的寄存器來實現的,所以先來了解一下HMC5883的寄存器情況。HMC5883的內部一共有12組寄存器,其中用於存放X、Y、Z三軸數據的寄存器有6個,餘下的6個是控制類寄存器,具體如下表所示。
地磁傳感器(HMC5883) - 西區故事 - 松柏後凋
和所有的IIC總線器件一樣,HMC5883也有一個器件的固定地址,根據其數據手冊,出廠時默認HMC5883的從機地址爲0x3C(寫入方向),或0x3D(讀出方向)。 同時,爲了儘量減少與單片機之間的通信,HMC5883可在無主機干預下自動更新其地址指針。指針更新有兩條原則,一是若訪問的地址是12(即識別寄存器C)或以上的地址時,指針會更新至地址00(即自動返回到開頭),二是若訪問的地址達到8(YLSB寄存器)時,指針會回滾到地址03(XMSB寄存器。這要做的好處顯而易見,因爲地址03~08存放的是要反覆讀取的數據測量值,所以讀取時地址指針自動在此循環,就可減小大量的重新設定地址的代碼,提高訪問效率。
同其它IIC器件一樣,要讓地址指針移動到一個指定的寄存器地址,首先要對該寄存器地址發出寫的指令,之後再跟一個地址位。例如要讓地址指針指向寄存器10,發出的指令爲0x3C(寫入方向)0x0A(即地址10)
配置寄存器A(地址00)主要是用來設置輸出採樣平均數、輸出速率和測量配置位等相關參數,對於常規應用可取其默認值(採樣平均數8,輸出速率15Hz,正常測量配置),不用去改動它。若實在要改,可詳細參閱管方的數據手冊。
配置寄存器B(地址01)主要是用來設置增益的,對於常規應用也可取其默認值,不用去改動它。若實在要改,可詳細參閱管方的數據手冊。
模式寄存器地址02)是用來選擇HMC5883的工作模式的,它一共有三種工作模式,即連續測量模式(最後兩位爲00)、單次測量模式(最後兩位爲01)和空閒模式(最後兩位爲10或11)。默認是單次測量模式,一般需要把它改爲連續測量模式。更改時只需要把該寄存器的最後兩位改爲00即可。
狀態寄存器地址09)主要是用來提供器件當前的狀態。它只有最後兩位有效,最後一位是準備就緒位,只有在準備就緒置位後才能對器件進行操作。倒數第二位是數據輸出寄存器鎖存位,當該位被置位時,任何的測量數據都不會被更新,直到測量數據被讀取。一般常規應用可通過適當的延時來進行讀取,而不必讀取該寄存器的狀態,除非在讀取的頻率很高時才考慮讀取此寄存器的狀態。
識別寄存器A(地址10)到識別寄存器C(地址12)這裏用不到,就不討論了,需要的請自行閱讀數據手冊。
其時HMC5883還有其它一些實用的功能,比如能夠進行自我檢測,它自身配備了自測功能模塊,利用激勵傳感器偏移帶產生一個待測的標稱磁場強度來進行自我檢測,以證明其好壞。此外還有比例因數的校準功能,它可以補償周圍磁場產生的干擾,以得到精確的地磁測量值。
下面就以一個例子來看一下HMC5883的具體應用。
例子:利用單片機讀取來自HMC5883的地磁數據,並把它轉換爲與正南方的夾角數據,通過LCD1602顯示出來。
單片機用ATMega16,與HMC5883的接法採用5V的方式。HMC5883SDASCL端分別接到ATMega16TWI端(PC1PC0),LCD1602的接法與前面的一致。參考代碼如下。
#include <iom16.h>
#include <math.h>
//=========================定義從器件地址和讀寫方式=============================
#define rd_device_add 0x3d //即00111101,0011110是HMC5883的固定地址,最後的1表示對從器件進行讀操作
#define wr_device_add 0x3c //即00111100,0011110是HMC5883的固定地址,最後的0表示對從器件時行寫操作
//===============================TWI狀態定義==================================
#define START 0x08
#define RE_START  0x10
#define MT_SLA_ACK  0x18
#define MT_SLA_NOACK  0x20
#define MT_DATA_ACK 0x28
#define MT_DATA_NOACK 0x30
#define MR_SLA_ACK  0x40
#define MR_SLA_NOACK  0x48
#define MR_DATA_ACK 0x50
#define MR_DATA_NOACK 0x58
//=============================常用TWI操作定義================================
#define Start() (TWCR=(1<<TWINT)|(1<<TWSTA)|(1<<TWEN))
#define Stop()  (TWCR=(1<<TWINT)|(1<<TWSTO)|(1<<TWEN))
#define Wait()  {while(!(TWCR&(1<<TWINT)));}
#define TestAck() (TWSR&0xf8)
#define SetAck()  (TWCR|=(1<<TWEA))
#define SetNoAck()  (TWCR&=~(1<<TWEA))
#define Twi() (TWCR=(1<<TWINT)|(1<<TWEN))
#define Write8Bit(x)  {TWDR=(x);TWCR=(1<<TWINT)|(1<<TWEN);}
//============引腳電平的宏定義===============
#define LCM_RS_1 PORTB_Bit0=1     //RS腳輸出高電平
#define LCM_RS_0 PORTB_Bit0=0       //RS腳輸出低電平
#define LCM_RW_1 PORTB_Bit1=1       //RW腳輸出高電平
#define LCM_RW_0 PORTB_Bit1=0       //RW腳輸出低電平
#define LCM_EN_1 PORTB_Bit2=1      //EN腳輸出高電平 
#define LCM_EN_0 PORTB_Bit2=0       //EN腳輸出低電平
#define DataPort PORTA          //PORTA爲數據端口
#define Busy 0x80         //忙信號
//==============定義變量================
unsigned char ge,shi,bai,qian,wan;           //顯示變量
unsigned char BUF[8];                         //接收數據緩存區
//==============定義顯示字符串================
const unsigned char str0[]={"Angle:   .      "};    //顯示角度
//===============1mS延時===================
void delay_1ms(void) 
{ unsigned int i;
 for(i=1;i<(unsigned int)(8*143-2);i++)
    ;
}
//=============n*1mS延時===============
void delay_nms(unsigned int n) 
{
 unsigned int i=0;
   while(i<n)
   {delay_1ms();
    i++;
   }
}
//===============IIC總線寫n個字節(成功返回0,失敗返回1)=====================
unsigned char I2C_Write(unsigned char RomAddress,unsigned char *buf,unsigned char len)
{
  unsigned char i;
  Start();                                                  //啓動I2C總線
  Wait();                                                   //等待迴應
  if(TestAck()!=START)                       
    return 1;                                              //若迴應的不是啓動信號,則失敗返回1
  Write8Bit(wr_device_add);                    //寫I2C從器件地址、寫方向
  Wait();                                                   //等待迴應
  if(TestAck()!=MT_SLA_ACK)                
    return 1;                                               //若迴應的不是ACK信號,則失敗返回值1                               
  Write8Bit(RomAddress);                         //寫HMC5883的ROM地址
  Wait();                                                     //等待迴應
  if(TestAck()!=MT_DATA_ACK)
    return 1;                                                 //若迴應的不是ACK信號則失敗返回值1
  for(i=0;i<len;i++)
    {
      Write8Bit(buf[i]);                          //寫數據到HMC5883的ROM中
      Wait();                                                      //等待迴應
      if(TestAck()!=MT_DATA_ACK)
        {return 1;}                                     //若迴應的不是ACK信號則失敗返回值1
      delay_nms(10);
    }
  Stop();                                                      //停止I2C總線
  delay_nms(10);                                          //延時等待HMC5883寫完
  return 0;                                                   //寫入成功,返回值0
}
//====================IIC總線讀n個字節(成功返回0,失敗返回1)=========================
unsigned char I2C_Read(unsigned char RomAddress,unsigned char *buf,unsigned char len)
{
  unsigned char i;
  Start();                                                     //啓動I2C總線
  Wait();                                                      //等待迴應
  if(TestAck()!=START)
    return 1;                                                  //若迴應的不是啓動信號,則失敗返回1
  Write8Bit(wr_device_add);                        //寫I2C從器件地址、寫方向
  Wait();                                                       //等待迴應
  if(TestAck()!=MT_SLA_ACK)
    return 1;                                                   //若迴應的不是ACK信號,則失敗返回值1
  Write8Bit(RomAddress);                             //寫HMC5883的ROM地址
  Wait();                                                         //等待迴應
  if(TestAck()!=MT_DATA_ACK)
    return 1;                                                     //若迴應的不是ACK信號,則失敗返回值1
  Start();                                                          //重新啓動I2C總線
  Wait();                                                           //等待迴應
  if(TestAck()!=RE_START)
    return 1;                                                      //若迴應的不是重複啓動信號,則失敗返回1
  Write8Bit(rd_device_add);                            //寫I2C從器件地址、讀方向
  Wait();                                                           //等待迴應
  if(TestAck()!=MR_SLA_ACK)
    return 1;                                                       //若迴應的不是ACK信號,則失敗返回值1
  for(i=0;i<len;i++)
    {
      Twi();                                                              //啓動I2C讀方式
      SetAck();                                           //設置接收自動應答
      delay_nms(10);
      Wait();                                                            //等待迴應
      delay_nms(10);
      *(buf+i)=TWDR;                                  //把連續讀取的len個字節數據依次存入對應的地址單元(數組)中
    }
  SetNoAck();                                         //讀數據的最後一位後緊跟的是無應答
  delay_nms(10);
  Stop();                                                            //停止I2C總線
  return 0;                                                    //成功返回值0
}
//================檢測LCD忙信號子函數================
void WaitForEnable(void)
{
    unsigned char val;
    DataPort=0xff;        //數據線電平拉高
    LCM_RS_0;             //選擇指令寄存器
    LCM_RW_1;             //選擇寫方式
    __asm("NOP");         //調用匯編指令延時一個空指令週期,等待穩定
    LCM_EN_1;             //使能端拉高電平
    __asm("NOP");
    __asm("NOP");         //調用匯編指令延時兩個空指令週期,等待穩定
    DDRA=0x00;            //改變數據線方向成輸入
    val=PINA;             //讀取數據
    while(val&Busy)
      val=PINA;           //當DB7位爲1時表示忙,循環檢測
    LCM_EN_0;             //忙信號結束,拉低使能端電平
    DDRA=0xff;            //改變數據線方向成輸出
}
//================寫數據到LCD子函數=================
void LcdWriteData(unsigned char dataW)      //寫數據dataW到LCD中
{
    WaitForEnable();        //檢測忙信號
    LCM_RS_1;               //選擇數據寄存器   
    LCM_RW_0;               //選擇讀方式
    __asm("NOP");           //調用匯編指令延時一個空指令週期,等待穩定
    DataPort=dataW;         //把顯示數據送到數據線上
    __asm("NOP");           //調用匯編指令延時一個空指令週期,等待穩定
    LCM_EN_1;               //使能端拉高電平
    __asm("NOP");
    __asm("NOP");           //調用匯編指令延時兩個空指令週期,等待穩定
    LCM_EN_0;               //拉低使能端,執行寫入動作
}
//================寫命令到LCD子函數================
void LcdWriteCommand(unsigned char CMD,unsigned char Attribc)   //寫命令CMD到LCD中,Arribc爲1時檢測忙信號,否則不檢測
{
    if(Attribc)
      WaitForEnable();        //檢測忙信號
    LCM_RS_0;                 //選擇指令寄存器
    LCM_RW_0;                 //選擇寫方式
    __asm("NOP");             //調用匯編指令延時一個空指令週期,等待穩定
    DataPort=CMD;             //把命令數據送到數據線上
    __asm("NOP");             //調用匯編指令延時一個空指令週期,等待穩定  
    LCM_EN_1;                 //使能端拉高電平
    __asm("NOP");
    __asm("NOP");             //調用匯編指令延時兩個空指令週期,等待穩定
    LCM_EN_0;                 //拉低使能端,執行寫入動作
}
//================顯示光標定位子函數================
void LocateXY(char posx,char posy)      //定位位置到地址x列y行
{
    unsigned char temp;
    temp=posx&0x0f;       //屏蔽高4位,限定x座標的範圍爲0~15
    posy&=0x01;           //屏蔽高7位,限定y座標的範圍爲0~1
    if(posy)       
      temp|=0x40;         //若要顯示的是第二行,則地址碼+0x40,因爲第二行起始地址爲0x40
    temp|=0x80;           //指令碼爲地址碼+0x80,因爲寫DDRAM時DB7恆爲1(即0x80)
    LcdWriteCommand(temp,1);    //把temp寫入LCD中,檢測忙信號
}
//===========顯示指定座標的一個字符子函數============
void DisplayOneChar(unsigned char x,unsigned char y,unsigned char Wdata)    //在x列y行處顯示變量Wdata中的一個字符
{
    LocateXY(x,y);          //定位要顯示的位置
    LcdWriteData(Wdata);    //將要顯示的數據Wdata寫入LCD
}
//==========顯示指定座標的一串字符子函數===========
void ePutstr(unsigned char x,unsigned char y,unsigned char const *ptr)    //在x列y行處顯示ptr指向的字符串
{
    unsigned char i,j=0;
    while(ptr[j]>31)
      j++;                         //ptr[j]>31時爲ASCII碼,j累加,計算出字符串長度
    for(i=0;i<j;i++)
    {
      DisplayOneChar(x++,y,ptr[i]);       //顯示單個字符,同時x座標遞增
      if(x==16)
        {
          x=0;
          y^=1;         //當每行顯示超過16個字符時換行繼續顯示
        }
    }
}
//==================LCD初始化子函數==================
void InitLcd(void)
{
    LcdWriteCommand(0x38,0);      //8位數據方式,雙行顯示,5X7字形,不檢測忙信號
    delay_nms(5);                          //延時5ms
    LcdWriteCommand(0x38,0);
    delay_nms(5);
    LcdWriteCommand(0x38,0);
    delay_nms(5);                          //重複三次
    LcdWriteCommand(0x38,1);      //8位數據方式,雙行顯示,5X7字形,檢測忙信號
    LcdWriteCommand(0x08,1);      //關閉顯示,檢測忙信號
    LcdWriteCommand(0x01,1);      //清屏,檢測忙信號
    LcdWriteCommand(0x06,1);      //顯示光標右移設置,檢測忙信號
    LcdWriteCommand(0x0C,1);      //打開顯示,光標不顯示,不閃爍,檢測忙信號
}
//================HMC5883初始化===================
void InitHMC5883(void)
{
  unsigned char t=0x00; 
  I2C_Write(0x02,&t,1);     //寫模式寄存器(地址0x02),設置爲連續測量模式(0x00)
}
//==================轉換子函數====================
void conversion(unsigned int temp_data) 

    wan=temp_data/10000+0x30 ;
    temp_data=temp_data%10000;   //取餘運算
    qian=temp_data/1000+0x30 ;
    temp_data=temp_data%1000;    //取餘運算
    bai=temp_data/100+0x30   ;
    temp_data=temp_data%100;     //取餘運算
    shi=temp_data/10+0x30    ;
    temp_data=temp_data%10;      //取餘運算
    ge=temp_data+0x30;  
}
//==================主函數=====================
void main(void)   
{
  int x,y;
  double angle;
  delay_nms(400);                    //延時400ms等待電源穩定
  DDRA=0xff;PORTA=0x00;
  DDRB=0xff;PORTB=0x00;
  DDRC=0xff;PORTC=0xff;
  DDRD=0xff;PORTD=0xff;         //初始化I/O口
  InitLcd();                                 //LCD初始化
  InitHMC5883();            //HMC5883初始化
  ePutstr(0,0,str0);         //顯示初始字符
  while(1)
  {
    I2C_Read(0x03,BUF,6);      //連續讀出6組數據,存儲在BUF中
    x=BUF[0]<<8|BUF[1];          //把X方向合併爲十六位數據
    y=BUF[4]<<8|BUF[5];          //把Y方向合併爲十六位數據
    angle=atan2((double)y,(double)x)*(180/3.14159265)+180;         // 換算成角度
    angle*=10;
    conversion((unsigned int)angle);       //計算數據和顯示
    DisplayOneChar(6,0,qian);           //顯示百位數據
    delay_nms(10);
    DisplayOneChar(7,0,bai);             //顯示十位數據
    delay_nms(10);
    DisplayOneChar(8,0,shi);             //顯示個位數據
    delay_nms(10);
    DisplayOneChar(10,0,ge);            //顯示小數點後一位數據
    delay_nms(50);            //延時
  }
}
在上述程序中,由於要計算反正切值用到函數atan2,所以要包含數學庫math.h。由於是在水平方向上測量地磁,因此沒用到Z軸的數據,只用了XY軸的數據。在此需要特別注意一點:由於使用了math.h中的atan2函數,在IAR開發環境中需要把編譯優化選項改成LowMedium(具體參見第一章,系統才能正常運行。若選擇不優化(None)時,程序會卡死在atan2函數中出不來,結果將得不到測量數據。究其原因可能與IAR的編譯環境有關,這一點要非常注意!!
把程序下載到單片機中,按要求接好連線,給系統上電,就可以在液晶屏上看到數據了。水平轉動HMC5883,可以看到顯示的角度隨之改變,實時測量出了與地球正南方的夾角。
發佈了12 篇原創文章 · 獲贊 8 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章