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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章