0xAA55 发表于 2017-11-2 20:01:01

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

之所以说强行写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则只能乖乖当“奴隶”。
https://cdn.sparkfun.com/r/600-600/assets/3/d/1/b/6/51adfda8ce395f151b000000.png
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();
}https://cdn.sparkfun.com/assets/6/4/7/1/e/51ae0000ce395f645d000000.png
嗯顺带一提这个图只是给你当参考的。它并不靠谱。我的代码才是最靠谱的。
那么一个完整的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就可以了。
嗯其它寄存器啥的各位自己看寄存器表网站吧,我就懒得赘述了。我直接把源码贴这儿,要的来下载啊。

顺带一提我修改了一下这玩意儿的采样率。然后以每秒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这个玩意儿的。

参考资料:

[*]提问:i2c support on Banana Pi M2 (A31s)
https://forum.armbian.com/topic/1527-i2c-support-on-banana-pi-m2-a31s/
[*]BPI-Mainline-kernel
https://github.com/BPI-SINOVOIP/BPI-Mainline-kernel
[*]i2c-tools源码
https://github.com/ev3dev/i2c-tools
[*]wiringPi的i2c的功能
http://wiringpi.com/reference/i2c-library/
[*]MPU-6000的PDF,凑合看吧,虽然不是6050的。
https://www.invensense.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf
[*]MPU-6050寄存器表(鼠标停留有详细的说明哦,我超喜欢这网站)
https://www.i2cdevlib.com/devices/mpu6050#registers
隐藏内容就是那个源码的内容而已回帖就可以直接看了。要是我是你的话我就直接下载源码了。**** Hidden Message *****

Gamma 发表于 2017-11-2 20:31:34

老大花心思了

Golden Blonde 发表于 2017-11-3 19:41:24

A5绝对是编程多面手(用FACEBOOK的话就是Full Stack Engineer),从PC到手机到嵌入式,就没有不会的。

(⊙o⊙) 发表于 2017-11-5 18:39:59

回复就说支持

(⊙o⊙) 发表于 2017-11-7 23:36:50


回复就说支持

安拉 发表于 2017-12-24 02:43:19

老大威武

安拉 发表于 2017-12-24 02:45:29

网站在云南备案,莫非老大是云南银

yzw92 发表于 2019-2-20 06:57:14

感谢楼主的精彩分享
页: [1]
查看完整版本: 【嵌入式】强行写GPIO实现I2C通讯,从MPU6050读取加速度、温度、陀螺仪方向