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

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 5552|回复: 7

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

[复制链接]

1109

主题

1649

回帖

7万

积分

用户组: 管理员

一只技术宅

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

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

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

×
之所以说强行写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再拉低。
  1. static void _i2c_delay()
  2. {
  3.         // usleep(5);
  4. }

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

嗯顺带一提这个图只是给你当参考的。它并不靠谱。我的代码才是最靠谱的。
那么一个完整的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。同样和刚才一样。用于判断传输是否成功。
  • 结束信号。
  1. #define i2c_pin scl_pin, sda_pin

  2. static void _i2c_write_byte(int scl_pin, int sda_pin, int value)
  3. {
  4.         pinMode(scl_pin, OUTPUT);
  5.         pinMode(sda_pin, OUTPUT);
  6.         _i2c_send_bit(i2c_pin, value & 0x80 ? 1 : 0);
  7.         _i2c_send_bit(i2c_pin, value & 0x40 ? 1 : 0);
  8.         _i2c_send_bit(i2c_pin, value & 0x20 ? 1 : 0);
  9.         _i2c_send_bit(i2c_pin, value & 0x10 ? 1 : 0);
  10.         _i2c_send_bit(i2c_pin, value & 0x08 ? 1 : 0);
  11.         _i2c_send_bit(i2c_pin, value & 0x04 ? 1 : 0);
  12.         _i2c_send_bit(i2c_pin, value & 0x02 ? 1 : 0);
  13.         _i2c_send_bit(i2c_pin, value & 0x01 ? 1 : 0);
  14. }

  15. int i2c_write_reg8(int scl_pin, int sda_pin, int chip_addr, int reg, int value)
  16. {
  17.         // start condition
  18.         _i2c_start(i2c_pin);
  19.        
  20.         // address & R/W bit (write => 0)
  21.         _i2c_write_byte(i2c_pin, (chip_addr << 1));
  22.        
  23.         // ACK
  24.         pinMode(sda_pin, INPUT);
  25.         if(_i2c_read_bit(i2c_pin))
  26.         {
  27.                 return 0; // NACK
  28.         }
  29.        
  30.         // register
  31.         _i2c_write_byte(i2c_pin, reg);
  32.        
  33.         // ACK
  34.         pinMode(sda_pin, INPUT);
  35.         if(_i2c_read_bit(i2c_pin))
  36.         {
  37.                 return 0; // NACK
  38.         }
  39.        
  40.         // value
  41.         _i2c_write_byte(i2c_pin, value);
  42.        
  43.         // ACK
  44.         pinMode(sda_pin, INPUT);
  45.         if(_i2c_read_bit(i2c_pin))
  46.         {
  47.                 return 0; // NACK
  48.         }
  49.        
  50.         // stop condition
  51.         _i2c_stop(i2c_pin);
  52.         return 1;
  53. }
复制代码
然后假设我们要从一个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,一切搞定。发送结束信号。
  1. static int _i2c_read_byte(int scl_pin, int sda_pin)
  2. {
  3.         int received = 0;
  4.        
  5.         pinMode(scl_pin, OUTPUT);
  6.         pinMode(sda_pin, INPUT);
  7.        
  8.         received |= _i2c_read_bit(i2c_pin) << 7;
  9.         received |= _i2c_read_bit(i2c_pin) << 6;
  10.         received |= _i2c_read_bit(i2c_pin) << 5;
  11.         received |= _i2c_read_bit(i2c_pin) << 4;
  12.         received |= _i2c_read_bit(i2c_pin) << 3;
  13.         received |= _i2c_read_bit(i2c_pin) << 2;
  14.         received |= _i2c_read_bit(i2c_pin) << 1;
  15.         received |= _i2c_read_bit(i2c_pin);
  16.        
  17.         // send NACK
  18.         pinMode(sda_pin, OUTPUT);
  19.         _i2c_send_bit(i2c_pin, 1);
  20.         return received;
  21. }

  22. int i2c_read_reg8(int scl_pin, int sda_pin, int chip_addr, int reg, int *received)
  23. {
  24.         // start condition
  25.         _i2c_start(i2c_pin);
  26.        
  27.         // address & R/W bit (write => 0)
  28.         _i2c_write_byte(i2c_pin, (chip_addr << 1));
  29.         // ACK
  30.         pinMode(sda_pin, INPUT);
  31.         if(_i2c_read_bit(i2c_pin))
  32.         {
  33.                 return 0; // NACK
  34.         }
  35.        
  36.         // register
  37.         _i2c_write_byte(i2c_pin, reg);
  38.         // ACK
  39.         pinMode(sda_pin, INPUT);
  40.         if(_i2c_read_bit(i2c_pin))
  41.         {
  42.                 return 0; // NACK
  43.         }
  44.        
  45.         // repeated start
  46.         _i2c_start(i2c_pin);
  47.        
  48.         // address & R/W bit (read => 1)
  49.         _i2c_write_byte(i2c_pin, (chip_addr << 1) | 1);
  50.         // ACK
  51.         pinMode(sda_pin, INPUT);
  52.         if(_i2c_read_bit(i2c_pin))
  53.         {
  54.                 return 0; // NACK
  55.         }
  56.        
  57.         // read register
  58.         *received = _i2c_read_byte(i2c_pin);
  59.        
  60.         // stop condition
  61.         _i2c_stop(i2c_pin);
  62.         return 1;
  63. }
复制代码
使用以上的代码,你只要插对了GPIO的线,就能和MPU6050进行数据交互,读写它的寄存器了。顺带一提我这里说的MPU6050是【GY-521 MPU-6050模块】价格从4元到万元不等。都一样。我的是4元的。再顺带一提,我并没有收广告费来着。我只是随手搜了一下找了个看起来最便宜的。是好是坏大家自己看着办。
不过如果你对焊接这种贴片很有自信的话你可以试试直接买MPU6050的贴片

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

  4. for(i = 0; i <= 0x7F; i++)
  5. {
  6.         if(i2c_read_reg8(I2C_GPIO_PIN, i, 0x75, &received))
  7.         {
  8.                 if(received == i)
  9.                 {
  10.                         chip_addr = i;
  11.                         printf("Found MPU6050 at 0x%X\n", i);
  12.                         break;
  13.                 }
  14.         }
  15. }

  16. if(chip_addr == -1)
  17. {
  18.         printf("MPU6050 not found.\n");
  19.         return 1;
  20. }
复制代码
找到这玩意儿后,你就可以跟它通讯了。按照刚才说的,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)
顺带一提我修改了一下这玩意儿的采样率。然后以每秒1000次以内的速度打印测量值,发现它采样率好像没跟上。后面发现其实就是_i2c_delay()函数体内的usleep(5)的延迟太大了,远超5us,导致I2C传输性能降低了。我把它注释掉后,我注意到它采样的速度明显提升。

……好像还有人想找我要makefile的源码……唉真没办法!拿去吧。
  1. CC = gcc
  2. LD = gcc
  3. CFLAGS = -Wall -lwiringPi

  4. all:i2c
  5. i2c: i2c.o
  6.         $(LD) -o $@ $^ $(CFLAGS)

  7. clean:
  8.         rm -f i2c.o i2c
复制代码
嗯我这个依赖wiringPi这个玩意儿的。

参考资料:
隐藏内容就是那个源码的内容而已回帖就可以直接看了。要是我是你的话我就直接下载源码了。
游客,如果您要查看本帖隐藏内容请回复
回复

使用道具 举报

1

主题

4

回帖

17

积分

用户组: 初·技术宅

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

使用道具 举报

55

主题

271

回帖

9330

积分

用户组: 管理员

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

使用道具 举报

1

主题

83

回帖

89

积分

用户组: 小·技术宅

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

使用道具 举报

1

主题

83

回帖

89

积分

用户组: 小·技术宅

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 | 显示全部楼层
网站在云南备案,莫非老大是云南银
回复 赞! 靠!

使用道具 举报

0

主题

28

回帖

4278

积分

用户组: 真·技术宅

UID
3513
精华
0
威望
12 点
宅币
4226 个
贡献
0 次
宅之契约
0 份
在线时间
381 小时
注册时间
2018-3-1
发表于 2019-2-20 06:57:14 | 显示全部楼层
感谢楼主的精彩分享
回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-3-29 02:59 , Processed in 0.048109 second(s), 34 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

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