可穿戴设备
智能手环是一种穿戴式智能设备。通过该设备,用户可以记录日常生活中的锻炼、睡眠等实时数据,并将这些数据与手机、平板同步,起到通过数据指导健康生活的作用。另外,智能手环还具有社交功能,能够将锻炼情况和睡眠质量发送到社交网络进行分享。
图 1_1某款智能手环
一个智能手环最小系统一般包括:可充电的电源模块、控制模块(图1_2中左边芯片)、蓝牙模块(右边芯片)、存储模块和加速计模块(上面芯片)。其中加速计是为了获得佩戴者在运动或睡眠过程中的加速度数据,通过分析这些数据则能够判断佩戴者的运动情况和睡眠质量;存储模块主要负责将实时数据暂存,接着在适当的时刻借助蓝牙模块将数据同步到手机端。方便起见本次要自制的记步手环将不采用存储器暂存,而是将数据实时地传送到手机端。同时为了便于大家对记步算法的理解,客户端将采用一个折线图的形式实时展示记步手环收集的数据。
图 1_2某款智能手环核心电路板
看了上面的分析大家可能会疑惑——仅仅用一个加速计怎么能实现记步和睡眠质量检测呢?其实确实可以!因为加速计可以实时获取自身的XY三个轴向的加速度。当其静止时合加速度会在重力加速度附近波动;当佩戴者处于深度睡眠过程中时,其合加速度将呈现出长时间的稳定于重力加速度附近;当其随着运动的佩戴者手臂而做周期性摆动时,其数据也是有一定规律可循的。这样,设计时只要通过分析从加速计获的数据就能实现对运动或睡眠质量的记录。
上面已经提到:为了方便,我们并未采用存储器实现记步手环的离线记录,而是实时地将数据发送到客户端由一个可视化的折线图动态绘制结果。如图3_1所示系统中记步手环部分包含单片机模块、蓝牙模块、加速计模块和电源模块,这样通过单片机的协调可以实现将加速计模块的数据通过蓝牙实时地传送给客户端程序。在客户端部分则负责将收集到的实时数据以折线图的形式动态地展示出来,此外客户端中也加入一个滑动条来控制记步阈值来真正让大家明白其设计思想(真正商业化的智能手环多数采用的是先将有效数据保存在手环的小型存储器中,上位机周期性地将数据收集并同步到服务器端)。
图 3_1 预期效果图
如图4_1,相比于上一个无线小风扇该硬件构成反而比较简单:蓝牙模块依然采用我们比较熟悉的HC-06模块,对于加速度的测量采用四周飞行器上常采用的MPU6050模块。该模块不仅含有加速计的功能,还具有陀螺仪的功能,其在汽车防侧翻、相机云台稳定、机器人平衡、空中鼠标、姿态识别等众多领域都有应用,这里我们只是利用了它的加速计功能。此外要注意:图4_1所示的单片机模块的电源引脚被隐藏了,在真正设计连接时一定不要忽略这两个引脚!
图 4_1 硬件电路图
MPU-60X0是全球首例9轴运动处理器。它集成了3轴MEMS陀螺仪,3轴MEMS加速计,以及1个可扩展的数字运动处理器DMP(Digital Motion Processor)。如图5_1所示轴向是相对于加速计说的,当芯片水平静止放置时x轴和y轴的加速度分量几乎为0,z轴的加速度分量约为当地的重力加速度;而旋转极性则是对陀螺仪来说的,本次先不介绍。
图 5_1 MPU-60X0轴向和旋转的极性(来自MPU6050数据手册)
为何上面说9轴信号呢?因为MPU-60X0可用I2C接口连接一个第三方的数字传感器,比如磁力计。扩展之后就可以通过其I2C或SPI接口输出一个9轴的信号。也可以通过其I2C接口连接非惯性的数字传感器,比如压力传感器。(为什么特别提磁力计和压力传感器呢?因为在飞控方面,利用陀螺仪和加速计可以计算飞行器的倾角,从而调节飞行器平衡。但是只是调节平衡对方向没有概念也不能执行复杂任务,因此需要配备磁力计(也即电子罗盘传感器)。此外,由于飞行器在不同高度作业时,其周围的重力加速度也不同,这样会影响倾角的准确性,因此通过气压计计算所处高度然后计算实时加速度达到精确控制的效果。)
图 5_2 MPU-60X0典型工作电路(来自MPU6050数据手册)
MPU-60X0对陀螺仪和加速计分别用了三个16位的ADC,将其测量的模拟量转化为可输出的数字量。为了精确跟踪快速和慢速运动,传感器的测量范围是可控的,陀螺仪可测范围为±250,±500,±1000,±2000°/秒(dps),加速计可测范围为±2,±4,±8,±16g(重力加速度)。如图5_3是直接从16位ADC中读出的6轴的数据(从左到右依次为加速计X轴数据、Y轴数据、Z轴数据、陀螺仪X极数据、Y极数据、Z极数据):
图 5_3 MPU6050输出加速计和陀螺仪6轴的原始数据
但是这里的输出值并不是真正的加速度和角速度的值,上面说过,MPU是一个16位AD量程可程控的设备,这里设置的加速度传感器的测量量程为正负2g(这里的g为重力加速度),陀螺仪的量程为正负2000°/s。所以要用下面的公式进行转化:
图5_4 实际值计算公式
最后给大家推荐一款比较容易买到的MPU6050,如图5_5该模块将核心芯片和外围电路集成到一个模块上并留出八个引脚,本次使用只需用到上面四个即可(具体连接参考图4_1)。
图5_5 MPU6050模块
第二小节讲到当MPU6050随着运动的佩戴者手臂而做周期性摆动时,其数据也是有一定规律可循的。简单起见我们只分析合加速度:一个摆臂周期其合加速度会在重力加速度上下波动,如图6_1只要选取合适的阈值(黑线代表阈值),每次检测出合加速度大于该阈值则认为是一次摆臂,从而可以实现记步的功能。这里要特别说明下:如果想把你的手环推向市场,就要通过大量分析摆臂数据建立一套更好的记步算法,如果偷懒只用楼主的简单算法,小心产品推出后被用户的口水淹死(哈哈)!
图 6_1 摆臂时合加速度变化图
上次我们在使用蓝牙串口模块时使用过串口通信,由于51系列单片机将串口通信很多细节都封装到芯片内部,所以我们即使设计了串口驱动模块,也并没有真正了解串口通信的核心思想。其实串口协议的出现是为了构成一个总线线路,这样单片机只要使用比较少的引脚就能和比较多的设备进行通信了,这里要用到的I2C总线也具有相同的效果但又有些不同。
图 7_1I2C总线挂接多个设备图
I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。是微电子通信控制领域广泛采用的一种总线标准。它是同步通信的一种特殊形式,具有接口线少,控制方式简单,器件封装形式小,通信速率较高等优点。如图7_1采用I2C总线后CPU只要使用2个引脚便可和多个设备进行通信(其实每个采用I2C通信方式的设备都具有唯一的地址码,这样在总线中便能够被唯一识别),从而大大减少了引脚的使用。
在I2C总线中使用的两线为时钟线SCL和数据线SDA。所有的I2C主从设备都是只被这两根线连接起来的。每一个设备既可以作为发送方,也可以作为接收方,或者既可以作为发送发也可以作为接收方。在总线中的主设备一般起产生时钟信号和初始化通信的作用,从设备则负责响应主设备发出的命令。为了在总线上区分每一个设备,每一个从设备必须有一个唯一的地址。主设备一般不需要地址(一般为微处理器),因为从设备不能发送命令给主设备。
图 7_2 I2C总线中主从设备
这里要先介绍I2C总线中几个专有名词:
l 发送者:将数据发送到总线的设备
l 接收者:从总线接收数据的设备
l 主设备:产生时钟信号、启动通信、发送I2C命令和终止通信的设备
l 从设备:监听总线、能被主设备寻址的设备
l 多主设备:I2C能够拥有多个主设备,而且每个主设备都能够发送命令
l 仲裁:当多个主设备请求使用总线时,决定哪一个主设备可以占用的一个过程
l 同步:同步多个设备时钟信号的一个过程
上面是从宏观上对I2C总线介绍了下,接下来将深入细节研究其通信过程:
n 串行数据传送:
在总线备用时SDA和SCL都必须保持高电平状态,只有关闭I2C总线时才能使SCL钳位在低电平。在I2C总线数据传输时,在时钟线高电平期间,数据线上必须保持有稳定的逻辑电平(也就是说在数据传输期间只有时钟线低电平期间,才允许数据线上的电平发生变化)。
图 7_3 串行数据发送
因此在如图7_3中对于每一个时钟脉冲期间一比特的数据将会被传送,SDA只能在时钟信号为低电平时才能改变。下面是代码中发送一字节的函数:在循环体内每次将dat内的最高位移出到CY中,进而赋值给SDA(这时SCL为低,SDA可改变)。接着拉高SCL并保持5us,最后再拉低SCL实现一个时钟脉冲将dat中最高位送出。依此循环8次实现将dat全部传出。
//------------------------------------------------
//向I2C总线发送一个字节数据
//------------------------------------------------
void I2C_SendByte(uchar dat)
{
uchar i;
for (i=0; i《8; i++) //8位计数器
{
dat 《《= 1; //移出数据的最高位
SDA = CY; //送数据口
SCL = 1; //拉高时钟线
Delay5us(); //延时
SCL = 0; //拉低时钟线
Delay5us(); //延时
}
I2C_RecvACK();
}
n 开始和结束条件:
命令不会没有任何预兆直接发送的,每一个I2C命令的发送总是开始于开始条件并结束于终止条件。这里所谓的开始条件和终止条件起始也是由SCL和SDA组合形成的(如图7_4)。
图 7_4 开始和结束条件
如果时钟线保持高电平期间,数据线出现由高到低的电平变化,则会启动I2C总线,此时为I2C的起始信号:
//------------------------------------------------
//I2C起始信号
//------------------------------------------------
void I2C_Start()
{
SDA = 1; //拉高数据线
SCL = 1; //拉高时钟线
Delay5us(); //延时
SDA = 0; //产生下降沿
Delay5us(); //延时
SCL = 0; //拉低时钟线
}
若在时钟线保持高电平期间,数据线出现由低到高的电平变化,则会停止I2C总线的数据传输,此时为I2C的终止信号:
//------------------------------------------------
//I2C停止信号
//------------------------------------------------
void I2C_Stop()
{
SDA = 0; //拉低数据线
SCL = 1; //拉高时钟线
Delay5us(); //延时
SDA = 1; //产生上升沿
Delay5us(); //延时
}
开始条件之后I2C总线被认为是忙状态,只有当停止信号之后其他主设备才能使用该总线。此外,当开始条件之后主设备能够多次发出开始信号。这些开始信号和第一次发出的开始信号类似,他们后面经常会跟从设备的地址。这样可以方便实现在I2C总线忙期间,当前占线的主设备可以和不同的从设备进行通信。
n I2C数据传送:
I2C总线上传送的每一个字节均为8位,但是每启动一次I2C总线,其后的数据传送字节数是没有限制的。同时每传送一字节的数据后面都要跟随一个接收者回应的应答位(低电平为应答信号,高电平为非应答信号),当全部数据发送完毕后主设备发送终止信号。
图 7_5 数据传送图
所以在上面向I2C总线发送一字节的数据的代码的最后有一个I2C_RecvACK()函数。(如下)该函数负责接收接收者发送过来的应答信号,也即图7_5中的第9个时钟脉冲的期间的相应操作。
//------------------------------------------------
//I2C接收应答信号
//------------------------------------------------
bit I2C_RecvACK()
{
SCL = 1; //拉高时钟线
Delay5us(); //延时
CY = SDA; //读应答信号
SCL = 0; //拉低时钟线
Delay5us(); //延时
return CY;
}
要特别说明下:所有的数据位包括应答位都需要主设备产生时钟脉冲。如果从设备没有应答意味着将没有更多的数据要传送或者设备没有准备好传送。这时,主设备要么产生停止信号,要么重新发出开始条件。
图 7_6 应答信号
n I2C的7-bit地址:
上面说过每一个从设备都应该具有唯一的地址,这样主设备才能准确的寻址到每一个设备,而这些地址被统一规定为7比特。但是上面讲过I2C总线传输数据都是8比特传送,地址7比特岂不是少一位!其实紧跟地址还有一位用来表示是读操作还是写操作的标志位。如果该位为0表示主设备将要向从设备写数据,否则表示主设备将要从从设备读数据。在这8比特被发送后主设备能够持续地进行读或者写。如果主设备想和其他从设备进行通信,只要再次发送一个新的开始信号就可以而不必发送终止信号。
图 7_7 一个完整的数据读写操作
至此,我们基本上已经将I2C的知识学完了,下面将结合MPU6050的驱动进一步讲解其原理(该部分的代码参见工程的mpu6050.c部分)。我们首先来看一下它的头文件mpu6050.h:从第6到25行上来就是一大串内部地址的定义,对于初学者可能一头雾水!如果楼主再引入寄存器等数字电路的知识可能又要说几页了,于是这里准备只用一个简单的例子阐述下这些地址的作用。
#include“i2c.h”
//-----------------------------------------
// 定义MPU6050内部地址
//-----------------------------------------
#define SMPLRT_DIV 0x19 //陀螺仪采样率,典型值:0x07(125Hz)
#define CONFIG 0x1A //低通滤波频率,典型值:0x06(5Hz)
#define GYRO_CONFIG 0x1B //陀螺仪自检及测量范围,典型值:0x18(不自检,2000deg/s)
#define ACCEL_CONFIG 0x1C //加速计自检、测量范围及高通滤波频率,典型值:0x01(不自检,2G,5Hz)
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48
#define PWR_MGMT_1 0x6B //电源管理,典型值:0x00(正常启用)
#define WHO_AM_I 0x75 //IIC地址寄存器(默认数值0x68,只读)
#define SlaveAddress 0xD0 //IIC写入时的地址字节数据,+1为读取
//-----------------------------------------
// 通过I2C和MPU6050通信的函数
//-----------------------------------------
void Single_WriteI2C(uchar REG_Address,uchar REG_data);//向I2C设备写入一个字节数据
uchar Single_ReadI2C(uchar REG_Address); //从I2C设备读取一个字节数据
void InitMPU6050(); //初始化MPU6050
int GetData(uchar REG_Address); //合成数据
上面讲到在I2C总线中主设备可以通过固定的7-bit地址寻找到相应的从设备(这里的7-bit地址为第26行的SlaveAddress,想必大家也能够理解后面注释的意义了吧~不加1表示紧跟着地址的一位为0,表示向该设备写数据;加1则表示紧跟着的一位为1,表示主设备从从设备读数据)。虽然采用这种方式能够准确找到从设备,但是从设备里面又有比较多的寄存器。这就好比你知道了某个要找的东西在具体的某个大柜子里,但是来到大柜子前又发现有许多小抽屉。这里的7-bit地址就好像指明了哪个柜子,而从第6到25行的内部地址就像柜子上的抽屉编号,而不一样之处是位于mpu6050内的“小抽屉”一部分存放着其采集的实时数据,另一部分等着外部放一些数据来设置其采样属性。
这样,如上面的第6行的SMPLRT_DIV(0x19)是用来设置陀螺仪采样率的寄存器地址,只要向该地址所指的寄存器写入相应的值则可以设置陀螺仪采样率。因此下面MPU6050初始化函数就是调用封装的I2C写函数向相应的小抽屉内写属性数据,设置MPU6050采样属性。
//------------------------------------------------
//初始化MPU6050
//------------------------------------------------
void InitMPU6050()
{
Single_WriteI2C(PWR_MGMT_1, 0x00); //解除休眠状态
Single_WriteI2C(SMPLRT_DIV, 0x07);
Single_WriteI2C(CONFIG, 0x06);
Single_WriteI2C(GYRO_CONFIG, 0x18);
Single_WriteI2C(ACCEL_CONFIG, 0x01);
}
再如第10~11行的ACCEL_XOUT_H、ACCEL_XOUT_L是用来存放最新的陀螺仪X极的数值,因为采用16位ADC所以这里需要用两个寄存器。所以下面合成数据函数负责连续读取REG_Address开始的两字节数据组成一个16位数据。当函数的参数为ACCEL_XOUT_H时,则获取的是实时的陀螺仪X极的数值,同样地可以获得实时的6轴数据。
//------------------------------------------------
//合成数据
//------------------------------------------------
int GetData(uchar REG_Address)
{
uchar H,L;
H=Single_ReadI2C(REG_Address);
L=Single_ReadI2C(REG_Address+1);
return (H《《8)+L; //合成数据
}
全部0条评论
快来发表一下你的评论吧 !