技术宅的结界

 找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 685|回复: 6
收起左侧

【嵌入式】强行写GPIO实现I2C通讯,从MPU6050读取加速度、温度、陀螺仪方向

[复制链接]

1004

主题

2229

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
199
威望
263 点
宅币
16626 个
贡献
33264 次
宅之契约
0 份
在线时间
1590 小时
注册时间
2014-1-26
发表于 2017-11-2 20:01:01 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?立即注册→加入我们

x
之所以说强行写GPIO,是因为我用的Banana Pi M2的镜像是armbian提供的,然后我在/dev里面找了半天,没看到有任何i2c设备在里面。如下文所示:
  1. root@bananapim2:~# gpio load i2c
  2. modprobe: FATAL: Module i2c-sunxi not found.
  3. gpio: Unable to load i2c-sunxi
复制代码
我在到处搜集资料看了一遍过来之后我觉得我大概搞明白了:armbian的香蕉派M2的镜像貌似比较老,没有编译新的源码。它的/boot/dtb目录里面有个“sun6i-a31s-sinovoip-bpi-m2.dtb”,然后这玩意儿的源码我看了,并没有定义i2c设备。最新的源码“BPI-Mainline-kernel”里面倒是定义了i2c设备,但我暂时不想编译这玩意儿。懒得折腾,干脆拿旧镜像凑合算了。(其实我试过把最新的内核编译了一遍,然后把sun6i-a31s-bananapi-m2.dtb改名为sun6i-a31s-sinovoip-bpi-m2.dtb之后写到原先armbian系统的/boot/dtb里面,再启动,结果系统起不来了。只好用虚拟机mount一下把它再改回去。)

我知道有i2cdetect这种玩意儿,我知道wiringPi集成了i2c的功能,但我的/dev目录下没有i2c设备我也用不了这些。
不如干脆自己造个轮子实现它,顺带还能让别的GPIO口也能进行i2c通讯,不需要依靠钦定的GPIO口,摆脱这个限制。而且自己写的代码如果可行了还可以顺手用到STM32上。(其实是懒得看别人的源码)

I2C非常简单粗放。它用两根线通信。一个是SDA,一个是SCL。
SDA负责数据的传输,SCL负责时钟。
然后SDA和SCL把所有的I2C的master和slave设备串联在一起。所有设备的SDA都是串联的,SCL也是串联的。这玩意儿叫I2C总线。英文名I2C bus。
master全权控制时钟,而数据的交互则根据情况,互相都有读写的过程。只有master能发起I2C传输过程,而slave则只能乖乖当“奴隶”。

I2C的数据传输协议也很简单,首先我们需要知道“开始信号”,也就是开始传输的条件是:
1、首先SDA和SCL都是高电平。
2、SDA先拉低。
3、然后SCL再拉低。
[C] 纯文本查看 复制代码
static void _i2c_delay()
{
	// usleep(5);
}

static void _i2c_start(int scl_pin, int sda_pin)
{
	pinMode(scl_pin, OUTPUT);
	pinMode(sda_pin, OUTPUT);
	
	// start condition
	digitalWrite(scl_pin, HIGH);
	_i2c_delay();
	digitalWrite(sda_pin, HIGH);
	_i2c_delay();
	digitalWrite(sda_pin, LOW);
	_i2c_delay();
	digitalWrite(scl_pin, LOW);
	_i2c_delay();
}
SDA拉低的时候延迟个4、5微秒,再拉低SCL,就是一个有效的“开始信号”了。
发送了“开始信号”后,就可以传输bit了。要传输一个bit,你需要这样操作:
1、根据这个bit的值,拉高或者拉低SDA。这个bit为1的时候就拉高,0的时候拉低。
2、然后拉高SCL。
3、再拉低SCL。
下面的代码记得调用前把对应pin设为OUTPUT模式。
[C] 纯文本查看 复制代码
static void _i2c_send_bit(int scl_pin, int sda_pin, int value)
{
	digitalWrite(sda_pin, value ? HIGH : LOW);
	digitalWrite(scl_pin, HIGH);
	_i2c_delay();
	digitalWrite(scl_pin, LOW);
	_i2c_delay();
}
然后我们不仅要写bit,我们有时候还是需要读bit的。读取的方法也很简单:
1、拉高SCL。
2、等个4、5微秒后就可以读了。
3、拉低SCL。
下面的代码记得调用前把SDA的pin设为INPUT模式。
[C] 纯文本查看 复制代码
static int _i2c_read_bit(int scl_pin, int sda_pin)
{
	int received;
	digitalWrite(scl_pin, HIGH);
	_i2c_delay();
	received = digitalRead(sda_pin) == HIGH ? 1 : 0;
	digitalWrite(scl_pin, LOW);
	_i2c_delay();
	return received;
}
之前说了“开始信号”,现在说“结束信号”。
1、首先SDA和SCL都是低电平。
2、SCL先拉高。
3、SDA再拉高。
[C] 纯文本查看 复制代码
static void _i2c_stop(int scl_pin, int sda_pin)
{
	pinMode(scl_pin, OUTPUT);
	pinMode(sda_pin, OUTPUT);
	
	digitalWrite(scl_pin, LOW);
	digitalWrite(sda_pin, HIGH);
	_i2c_delay();
	digitalWrite(scl_pin, HIGH);
	_i2c_delay();
}

嗯顺带一提这个图只是给你当参考的。它并不靠谱。我的代码才是最靠谱的。
那么一个完整的I2C传输是怎样的呢?
假设我们要写一个8bit值到一个slave设备的某个寄存器里:
  • 开始信号。
  • 传输设备地址值(设备地址值为7个bit)
  • 传输“读、写”的bit。读为1,写为0。所以此时传输0,因为我们是要指定寄存器的值。
  • 从设备接受ACK bit。1表示“我不ACK”,翻译成中文就是“我不知道你在说什么”的意思。现在这种情况就是“我这个slave设备的地址和你说的不一样”。而如果刚才传输的那个设备地址上有对应设备,那么这个设备就会传输一个0给你,也就是“我ACK”。
    注意这个“设备地址值”是I2C总线上用来区分不同设备的一个玩意儿。毕竟大家都是串联在一起的。我们可以把这个当作设备的“名字”。先点名,再对话。
  • 好,现在指定好了设备以后,就可以传输寄存器地址了。嗯现在传输8个bit的寄存器地址。
  • 继续从SDA接收ACK bit,和刚才一样。你发现了吧,每传输一个字节,就要进行一次ACK判断。
  • 刚才已经传输了设备地址和寄存器号了。现在传输寄存器值。同样8个bit。
  • 传输完了,从SDA接收ACK bit。同样和刚才一样。用于判断传输是否成功。
  • 结束信号。
[C] 纯文本查看 复制代码
#define i2c_pin scl_pin, sda_pin

static void _i2c_write_byte(int scl_pin, int sda_pin, int value)
{
	pinMode(scl_pin, OUTPUT);
	pinMode(sda_pin, OUTPUT);
	_i2c_send_bit(i2c_pin, value & 0x80 ? 1 : 0);
	_i2c_send_bit(i2c_pin, value & 0x40 ? 1 : 0);
	_i2c_send_bit(i2c_pin, value & 0x20 ? 1 : 0);
	_i2c_send_bit(i2c_pin, value & 0x10 ? 1 : 0);
	_i2c_send_bit(i2c_pin, value & 0x08 ? 1 : 0);
	_i2c_send_bit(i2c_pin, value & 0x04 ? 1 : 0);
	_i2c_send_bit(i2c_pin, value & 0x02 ? 1 : 0);
	_i2c_send_bit(i2c_pin, value & 0x01 ? 1 : 0);
}

int i2c_write_reg8(int scl_pin, int sda_pin, int chip_addr, int reg, int value)
{
	// start condition
	_i2c_start(i2c_pin);
	
	// address & R/W bit (write => 0)
	_i2c_write_byte(i2c_pin, (chip_addr << 1));
	
	// ACK
	pinMode(sda_pin, INPUT);
	if(_i2c_read_bit(i2c_pin))
	{
		return 0; // NACK
	}
	
	// register
	_i2c_write_byte(i2c_pin, reg);
	
	// ACK
	pinMode(sda_pin, INPUT);
	if(_i2c_read_bit(i2c_pin))
	{
		return 0; // NACK
	}
	
	// value
	_i2c_write_byte(i2c_pin, value);
	
	// ACK
	pinMode(sda_pin, INPUT);
	if(_i2c_read_bit(i2c_pin))
	{
		return 0; // NACK
	}
	
	// stop condition
	_i2c_stop(i2c_pin);
	return 1;
}
然后假设我们要从一个slave设备里面读取一个8bit的寄存器的值,传输过程则和写寄存器的值的过程略有不同。
  • 开始信号。
  • 7个bit的设备地址,和1个bit的“写入”信号(也就是低电平)。嗯确实是写入,因为接下来我们是要把寄存器地址写入给slave的。
  • ACK。不用我多说了吧?
  • 8个bit的寄存器地址。
  • 继续ACK。
  • 结束信号。嗯还没完。刚才是在指定slave的寄存器。
  • 开始信号。这种“结束后马上开始”的信号被称作“重新开始信号”。
  • 7个bit的设备地址,还是刚才那个设备的地址。但这次我们是要读取寄存器的值,所以要再传输一个bit的“读取”信号,把1个高电平传输过去。
  • 你懂的。每传输8个bit你要读一次ACK来判断设备O不OK。
  • 接下来,就要读取8个bit了。这就是slave给你传输的寄存器值了。
  • 从slave读了数值之后咋办?当然是ACK了。不过且慢,这次是你发送ACK,哦不,是NACK,也就是你要发一个1回去给slave。1表示“我不ACK”,现在这个bit本身的含义是“你是不是不ACK”,你发1表示“不”,意思就是“我不·不ACK”,双重否定表示肯定。也就是“我ACK”的意思。
  • over,一切搞定。发送结束信号。
[C] 纯文本查看 复制代码
static int _i2c_read_byte(int scl_pin, int sda_pin)
{
	int received = 0;
	
	pinMode(scl_pin, OUTPUT);
	pinMode(sda_pin, INPUT);
	
	received |= _i2c_read_bit(i2c_pin) << 7;
	received |= _i2c_read_bit(i2c_pin) << 6;
	received |= _i2c_read_bit(i2c_pin) << 5;
	received |= _i2c_read_bit(i2c_pin) << 4;
	received |= _i2c_read_bit(i2c_pin) << 3;
	received |= _i2c_read_bit(i2c_pin) << 2;
	received |= _i2c_read_bit(i2c_pin) << 1;
	received |= _i2c_read_bit(i2c_pin);
	
	// send NACK
	pinMode(sda_pin, OUTPUT);
	_i2c_send_bit(i2c_pin, 1);
	return received;
}

int i2c_read_reg8(int scl_pin, int sda_pin, int chip_addr, int reg, int *received)
{
	// start condition
	_i2c_start(i2c_pin);
	
	// address & R/W bit (write => 0)
	_i2c_write_byte(i2c_pin, (chip_addr << 1));
	// ACK
	pinMode(sda_pin, INPUT);
	if(_i2c_read_bit(i2c_pin))
	{
		return 0; // NACK
	}
	
	// register
	_i2c_write_byte(i2c_pin, reg);
	// ACK
	pinMode(sda_pin, INPUT);
	if(_i2c_read_bit(i2c_pin))
	{
		return 0; // NACK
	}
	
	// repeated start
	_i2c_start(i2c_pin);
	
	// address & R/W bit (read => 1)
	_i2c_write_byte(i2c_pin, (chip_addr << 1) | 1);
	// ACK
	pinMode(sda_pin, INPUT);
	if(_i2c_read_bit(i2c_pin))
	{
		return 0; // NACK
	}
	
	// read register
	*received = _i2c_read_byte(i2c_pin);
	
	// stop condition
	_i2c_stop(i2c_pin);
	return 1;
}
使用以上的代码,你只要插对了GPIO的线,就能和MPU6050进行数据交互,读写它的寄存器了。顺带一提我这里说的MPU6050是【GY-521 MPU-6050模块】价格从4元到万元不等。都一样。我的是4元的。再顺带一提,我并没有收广告费来着。我只是随手搜了一下找了个看起来最便宜的。是好是坏大家自己看着办。
不过如果你对焊接这种贴片很有自信的话你可以试试直接买MPU6050的贴片

好接下来说一下MPU6050的寄存器。这玩意儿有0x76个寄存器。最后一个寄存器(地址0x75)是“WHO AM I”,它存储的是当前slave设备的设备地址。所以如何在一个I2C总线里面搜索MPU6050呢?靠的就是这个寄存器。
[C] 纯文本查看 复制代码
int received;
int i;
int chip_addr = -1;

for(i = 0; i <= 0x7F; i++)
{
	if(i2c_read_reg8(I2C_GPIO_PIN, i, 0x75, &received))
	{
		if(received == i)
		{
			chip_addr = i;
			printf("Found MPU6050 at 0x%X\n", i);
			break;
		}
	}
}

if(chip_addr == -1)
{
	printf("MPU6050 not found.\n");
	return 1;
}
找到这玩意儿后,你就可以跟它通讯了。按照刚才说的,I2C通讯的规则,总结下来(容我啰嗦),就是:
如果你要写这玩意儿的寄存器:
  • 开始信号。
  • 传输8个bit:(设备地址值 << 1)
  • 读取ACK状态。
  • 传输8个bit:寄存器号
  • 读取ACK状态。
  • 传输8个bit:寄存器值
  • 读取ACK状态。
  • 结束信号。

如果你要读这玩意儿的寄存器:
  • 开始信号。
  • 传输8个bit:(设备地址值 << 1)
  • 读取ACK状态。
  • 传输8个bit:寄存器号
  • 读取ACK状态。
  • 结束信号。
  • 开始信号。
  • 传输8个bit:(设备地址值 << 1) | 1
  • 读取ACK状态。
  • 读取8个bit:寄存器值
  • 写入NACK状态:1
  • 结束信号。

嗯现在确定无误了,I2C的通讯就是这么回事儿(其实还有16 bit或者32 bit的寄存器的读写我先懒得实现了)
我们需要知道的是如何从MPU6050读取加速度的值。嗯是寄存器表的网站拿走不谢。https://www.i2cdevlib.com/devices/mpu6050#registers

这玩意儿刚上电的时候是sleep状态。爆它菊!让它醒过来就可以读取它测量的数据了。
i2c_write_reg8(I2C_GPIO_PIN, chip_addr, 0x6B, 0x00);
把0x6B寄存器的值写入一个0就可以了。
嗯其它寄存器啥的各位自己看寄存器表网站吧,我就懒得赘述了。我直接把源码贴这儿,要的来下载啊。
i2c.c (6.33 KB, 下载次数: 0)

1

主题

5

帖子

17

积分

用户组: 初·技术宅

UID
3035
精华
0
威望
1 点
宅币
10 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2017-11-2
发表于 2017-11-2 20:31:34 | 显示全部楼层
老大花心思了

34

主题

133

帖子

6948

积分

用户组: 管理员

UID
77
精华
11
威望
112 点
宅币
6407 个
贡献
129 次
宅之契约
0 份
在线时间
91 小时
注册时间
2014-2-22
发表于 2017-11-3 19:41:24 | 显示全部楼层
A5绝对是编程多面手(用FACEBOOK的话就是Full Stack Engineer),从PC到手机到嵌入式,就没有不会的。

1

主题

86

帖子

91

积分

用户组: 小·技术宅

UID
3026
精华
0
威望
1 点
宅币
3 个
贡献
0 次
宅之契约
0 份
在线时间
6 小时
注册时间
2017-10-31
发表于 2017-11-5 18:39:59 | 显示全部楼层
回复就说支持

1

主题

86

帖子

91

积分

用户组: 小·技术宅

UID
3026
精华
0
威望
1 点
宅币
3 个
贡献
0 次
宅之契约
0 份
在线时间
6 小时
注册时间
2017-10-31
发表于 2017-11-7 23:36:50 | 显示全部楼层

回复就说支持

0

主题

9

帖子

31

积分

用户组: 初·技术宅

UID
3245
精华
0
威望
2 点
宅币
18 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2017-12-24
发表于 2017-12-24 02:43:19 | 显示全部楼层
老大威武
回复

使用道具 举报

0

主题

9

帖子

31

积分

用户组: 初·技术宅

UID
3245
精华
0
威望
2 点
宅币
18 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2017-12-24
发表于 2017-12-24 02:45:29 | 显示全部楼层
网站在云南备案,莫非老大是云南银

本版积分规则

QQ|申请友链|Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号|网站地图

GMT+8, 2018-11-13 06:25 , Processed in 0.119306 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表