- UID
- 1
- 精华
- 积分
- 76365
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
之所以说强行写GPIO,是因为我用的Banana Pi M2的镜像是armbian提供的,然后我在/dev里面找了半天,没看到有任何i2c设备在里面。如下文所示:- root@bananapim2:~# gpio load i2c
- modprobe: FATAL: Module i2c-sunxi not found.
- 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再拉低。
- 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模式。- 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模式。- 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再拉高。- 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。同样和刚才一样。用于判断传输是否成功。
- 结束信号。
- #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,一切搞定。发送结束信号。
- 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呢?靠的就是这个寄存器。- 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)
顺带一提我修改了一下这玩意儿的采样率。然后以每秒1000次以内的速度打印测量值,发现它采样率好像没跟上。后面发现其实就是_i2c_delay()函数体内的usleep(5)的延迟太大了,远超5us,导致I2C传输性能降低了。我把它注释掉后,我注意到它采样的速度明显提升。
……好像还有人想找我要makefile的源码……唉真没办法!拿去吧。- CC = gcc
- LD = gcc
- CFLAGS = -Wall -lwiringPi
- all:i2c
- i2c: i2c.o
- $(LD) -o $@ $^ $(CFLAGS)
- clean:
- rm -f i2c.o i2c
复制代码 嗯我这个依赖wiringPi这个玩意儿的。
参考资料:
隐藏内容就是那个源码的内容而已回帖就可以直接看了。要是我是你的话我就直接下载源码了。 |
|