最近在做嵌入式的一个实验,要求手撕i2c,不能调用gpio库和一些把i2c封装好的库,来读取MPU6050的数据。比如说wiringPi,smbus之类的库。
1. MPU6050
外部引脚功能
管脚名称 说明
VCC 3.3V-5V
GND 地线
SCL 时钟线(从机)
SDA 数据线(从机)
XCL 时钟线(主机)
XDA 数据线(主机)
AD0 地址管脚,见reg117(0x75)
INT 中断
软件配置及使用
Reg117(0x75) Who Am I
WHO_AM_I是MPU-60X0的7位I2C地址的高6位。 MPU-60X0的I2C地址的最低有效位由AD0引脚的值决定。 AD0引脚的值不会反映在该寄存器中。寄存器的默认值为0x68。第0位和第7位保留。 (硬编码为0)
Reg 107 (0x6B) Power Management 1
DEVICE_RESET设置为1时,该位将所有内部寄存器复位为默认值。复位完成后,该位自动清零。
Reg27(0x1B) Gyroscope Configuration
在FS_SEL[1:0]上写入合适的值,对应不同的量程(可以理解为灵敏度)
gx = gyr_x / 32768 * value[FS_SEL]
同上选择量程
Reg28(0x1C) ACCEL Configuration
ax = acc_x / 32768 * value[AFS_SEL] * g
Reg65 and 66(0x41 and 0x42) Temperature Measurement
Temperature in degrees C = (TEMP_OUT Register Value as a signed quantity)/340 + 36.53
2. i2c通讯
I2C接口I2C是一种双线接口,由信号串行数据(SDA)和串行时钟(SCL)组成。通常,线路是开漏和双向的。在通用I2C接口实现中,连接的设备可以是主设备或从设备。主设备将从设备地址放在总线上,具有匹配地址的从设备确认主设备。在与系统处理器通信时,MPU-60X0始终作为从设备运行,因此系统处理器充当主设备。 SDA和SCL线通常需要上拉电阻到VDD。最大总线速度为400 kHz。 MPU-60X0的从机地址为b110100X,长度为7位。 7位地址的LSB位由引脚AD0上的逻辑电平决定。这允许两个MPU-60X0连接到同一I2C总线。在此配置中使用时,其中一个器件的地址应为b1101000(引脚AD0为逻辑低电平),另一个器件的地址应为b1101001(引脚AD0为逻辑高电平)。
I2C数据字节定义为8位长。 I2C数据字节定义为8位长。每次数据传输的字节数没有限制。传输的每个字节后面必须跟一个确认(ACK)信号。确认信号的时钟由主机产生,而接收机通过拉低SDA并在应答时钟脉冲的高电平部分保持低电平来产生实际的确认信号。如果从器件忙,并且在执行某个其他任务之前无法发送或接收另一个数据字节,则它可以保持SCL为低电平,从而强制主器件进入等待状态。从机准备就绪后,正常数据传输恢复,并释放时钟线。
在开始与START条件(S)通信后,主机发送一个7位从机地址,然后是第8位,即读/写位。读/写位指示主设备是从从设备接收数据还是正在写入从设备。然后,主设备释放SDA线并等待来自从设备的确认信号(ACK)。传输的每个字节后面必须跟一个应答位。要确认,从器件将SDA线拉低,并在SCL线的高电平期间保持低电平。数据传输总是由主机以STOP条件(P)终止,从而释放通信线路。但是,主机可以生成重复的START条件(Sr),并在不首先生成STOP条件(P)的情况下寻址另一个从机。当SCL为高电平时,SDA线上的低电平到高电平转换定义了停止条件。所有SDA更改都应在SCL为低时进行,但启动和停止条件除外。
要写入内部MPU-60X0寄存器,主器件发送启动条件(S),然后发送I2C地址和写入位(0)。在第9个时钟周期(当时钟为高电平时),MPU-60X0确认传输。然后主设备将寄存器地址(RA)放在总线上。在MPU-60X0确认接收到寄存器地址后,主器件将寄存器数据放入总线。接下来是ACK信号,并且可以通过停止条件(P)结束数据传输。要在最后一个ACK信号之后写入多个字节,主设备可以继续输出数据而不是发送停止信号。在这种情况下,MPU-60X0自动递增寄存器地址并将数据加载到适当的寄存器。下图显示了单字节和双字节写序列。
要读取内部MPU-60X0寄存器,主器件发送一个启动条件,然后是I2C地址和一个写入位,然后是要读取的寄存器地址。在从MPU-60X0接收到ACK信号后,主机发送启动信号,然后发送从机地址和读取位。结果,MPU-60X0发送ACK信号和数据。通信以非应答(NACK)信号和来自主设备的停止位结束。定义NACK条件使得SDA线在第9个时钟周期保持高电平。下图显示了单字节和双字节读取序列。
3.手撕i2c
首先是宏定义,也可以不写,直接用地址,但是读上去不直观也不方便检查
#define SMPLRT_DIV 0x19
#define CONFIG 0x1A
#define GYRO_CONFIG 0x1B
#define ACCEL_CONFIG 0x1C
#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
#define WHO_AM_I 0x75
#define SlaveAddress 0xD0
#define Address 0x68 //MPU6050地址
#define Time 20 //i2c延时常数
函数声明(每个函数的作用应该很明确)
static uint8 MPU6050_Init(void);
void i2c_start(void);
void i2c_stop(void);
unsigned char i2c_read_ack(void);
void i2c_send_ack(void);
void i2c_send_noack(void);
void i2c_write_byte(unsigned char b);
unsigned char i2c_read_byte(void);
void i2c_read(unsigned char addr, unsigned char* buf, int len);
void i2c_write (unsigned char addr, unsigned char buff);
short GetData(unsigned char REG_Address);
GPIO传参(要和用户函数中的结构体成员相对应)
static struct gpio_config{
int SDA;
int SCL;
short ax;
short ay;
short az;
short gx;
short gy;
short gz;
short tem;
short name;
}config;
要初始化MPU及设置量程
MPU6050_Init();
config.ax = GetData(ACCEL_XOUT_H);
config.ay = GetData(ACCEL_YOUT_H);
config.az = GetData(ACCEL_ZOUT_H);
config.gx = GetData(GYRO_XOUT_H);
config.gy = GetData(GYRO_YOUT_H);
config.gz = GetData(GYRO_ZOUT_H);
config.tem = GetData(TEMP_OUT_H);
config.name = GetData(WHO_AM_I);
void i2c_start(void)
{
//初始化GPIO口
gpio_direction_output(config.SDA, 1); //设置config.SDA方向为输出
gpio_direction_output(config.SCL, 1); //设置config.SCL方向为输出
gpio_set_value(config.SDA, 1); //设置config.SDA为高电平
gpio_set_value(config.SCL, 1); //设置config.SCL为高电平
udelay(Time); //延时
//起始条件
gpio_set_value(config.SDA, 0); //config.SCL为高电平时,config.SDA由高变低
udelay(Time);
}
/* I2C终止条件 */
void i2c_stop(void)
{
gpio_set_value(config.SCL, 1);
gpio_direction_output(config.SDA, 0);
gpio_set_value(config.SDA, 0);
udelay(Time);
gpio_set_value(config.SDA, 1); //config.SCL高电平时,config.SDA由低变高
}
/*
I2C读取ACK信号(写数据时使用)
返回值 :0表示ACK信号有效;非0表示ACK信号无效
*/
unsigned char i2c_read_ack(void)
{
unsigned char r;
gpio_direction_input(config.SDA); //设置config.SDA方向为输入
gpio_set_value(config.SCL,1); // config.SCL变高
udelay(Time);
r = gpio_get_value(config.SDA); //读取ACK信号
udelay(Time);
gpio_set_value(config.SCL,0); // config.SCL变低
udelay(Time);
return r;
}
/* I2C发出ACK信号(读数据时使用) */
void i2c_send_ack(void)
{
gpio_set_value(config.SCL,0);
gpio_direction_output(config.SDA, 0); //设置config.SDA方向为输出,并发送ack
udelay(Time);
gpio_set_value(config.SCL,1); // config.SCL变高
udelay(Time);
gpio_set_value(config.SCL,0);
}
/* I2C发出noACK信号(读数据时使用) */
void i2c_send_noack(void)
{
gpio_set_value(config.SCL,0);
gpio_direction_output(config.SDA, 1); //设置config.SDA方向为输出,并发送noack
udelay(Time);
gpio_set_value(config.SCL,1); // config.SCL变高
udelay(Time);
gpio_set_value(config.SCL,0);
}
/* I2C字节写 */
void i2c_write_byte(unsigned char b)
{
int i;
gpio_direction_output(config.SDA, 0); //设置config.SDA方向为输出
udelay(Time);
for (i=7; i>=0; i--) {
gpio_set_value(config.SCL, 0); // config.SCL变低 (SCL为低时可以改变SDA数据,开始写)
udelay(Time);
gpio_set_value(config.SDA, b & (1<=0; i--) {
gpio_set_value(config.SCL, 1); // config.SCL变高(SCL为高时不能改变SDA数据,开始读)
udelay(Time);
r = (r <<1) | gpio_get_value(config.SDA); //从高位到低位依次准备数据进行读取
gpio_set_value(config.SCL, 0); // config.SCL变低
udelay(Time);
}
i2c_send_noack(); //向目标设备发送onack信号,表示一个字节读取完毕,请求终止
return r;
}
/*
I2C读操作
addr:目标设备地址
buf:读缓冲区
len:读入字节的长度
*/
void i2c_read(unsigned char addr, unsigned char* buf, int len)
{
int i;
unsigned char t;
i2c_start();
i2c_write_byte(0xD0); //起始条件,开始数据通信,低位为1,表示开始写数据
t = addr;
i2c_write_byte(t); //发送地址和数据读写方向
i2c_start();
i2c_write_byte(0xD1); //低位为1,表示读数据
for (i=0; i
写数据:先写从机地址加一位读/写位(写/0),得到从机应答后,再写寄存器地址,得到应答后,开始写数据,写完发送ack告诉从机写完了,i2c stop。
读数据:先写从机地址加一位读/写位(写/0),得到ack后,写寄存器地址,得到ack后,重新i2c start ,写从机地址加一位读/写位(读/1),得到ack后,开始读数据存入缓存区,由于博主都是一个字节一个字节读的,所以这里的len为1;读完之后发送noack,然后i2c stop终止通信;
由于有些寄存器有高位和低位,所以读取时要读2个字节,及i2c_read(reg_addr,&H)+i2c_read(reg_addr+1,&L);
博主在这里也是困惑了很久,最后在小伙伴的帮助下找出问题,实在感谢。
顺便把SDA也讲了吧
无数据:SCL=1,SDA=1;
开始位(Start):当SCL=1时,SDA由1向0跳变;
停止位(Stop):当SCL=1时,SDA由0向1跳变;
数据位:当SCL由0向1跳变时,由发送方控制SDA,此时SDA为有效数据,不可随意改变SDA;当SCL保持为0时,SDA上的数据可随意改变;
总结:
读数据时,把SCL从0置1,读完置0;
写数据时,把SCL置0,写完一位置1,再写下一位;最后把SCL置0;
在每一步处理的时候,要保证SCL状态为:初始为0和结束为0;