Linux driver i2c subsystem mpu6050

Foreword:

In fact, there is nothing new, because the Linux kernel implements the principle of separation of devices and drivers, and the process of i2c driver development is not much different from that of platform bus device driver development. This i2c subsystem is also the system that helps us prepare the bus. In terms of hardware peripheral information, we describe the i2c peripheral device and then register it or describe the i2c peripheral hardware information in the device tree. The device part is done. As for the i2c driver, it is the same as what we have learned before. Write the matching symbol table and proce function and then register the driver into the system. If the corresponding device is found in the system or a consistent one is found in the device tree Compatible, it will enter the probe function.

After successful matching, you can also get the hardware information of the peripheral based on the i2c protocol (one parameter of the process function is i2c_client, get it from here), which is usually the address of the peripheral. We all know i2c communication, and the host must know it. The address of i2c; of course, information such as clock can be defined as needed. The i2c subsystem does not help us generate device nodes, so we have to follow the old path of character device drivers and “hard work” to generate device nodes and respond to the application’s file operations.

There are several important concepts in the i2c subsystem.

1.i2c_adapter, the literal translation is adapter, but I think it is easier to understand called controller. Generally, a SOC has many i2c controllers, but we don’t need to worry about this. The Linux kernel has been written, and it actually drives the SOC. i2c controller. The following code will show that our i2c data transmission function, the owner of this member is i2c_adapter.

2.i2c_client, literal translation means client. My understanding is that it is the younger brother of adapter. In fact, it is used to describe the peripherals of the i2c interface mounted under i2c_adapter. This is equivalent to the role of “device” in the platform device driver model. We need to describe the hardware information. For example, what I want to write now is mpu6050. I need to write an mpu6050 subnode in the device tree controller and write the address and other information to it.

3.i2c_driver, this is of course the i2c driver. It needs to be written as a matching symbol table and what to do after writing the probe.

Code:

Device tree:

First of all, we need to determine which I2C GPIOs on our development board can still be used. I found the I2C_7 interface in the camera interface on the schematic diagram of the development board base plate. I did not find that CAMERA is configured in the kernel configuration. It should be possible. use.

In fact, from another angle, we can also see whether there are free i2c interfaces available.

From the i2c chapter of the chip manual, we can know that the 4412 SOC has a total of 9 i2c controllers, one for HDMI, so there are 8 more available.

By looking at /sys/bus/i2c/devices, you can see that several i2c controllers have been used. For learning, try to choose ones that have not been used.

After selecting I2C7 as the i2c controller, go to the device tree to find i2c and find the i2c7 controller:

This is a disi written by the SOC manufacturer. We generally don’t need to change the already written part, but we have to change the status to “okay”. Of course, we don’t change it here.

Come to our board-level device tree file exynos4412-itop-elite.dts

Write a node:

 &i2c_7{
status = "okay";
mpu6050@68{
compatible = "lch-mpu6050";
reg = h;

};
};

When this &i2c_7 is finally compiled, it will be merged with the previous i2c_7 with a lot of content, and there are differences. The dts file compiled in the future will be overwritten, so the status will eventually be “okay”, that is, turned on. and

mpu6050@68{
compatible = “lch-mpu6050”;//This is matched with the driver
reg = h;

};

For this child node, it is enough to write the device address. For this address, check the chip manual of mpu6050 (or the chip manual of other i2c peripherals). If there is other required information, it can be defined again.

15th_i2c.c

//A bunch of header files
#include <linux/init.h>
#include <linux/module.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>
#include <linux/i2c.h>
\t\t\t\t\t\t\t\t\t
#include <asm/io.h>
#include <linux/uaccess.h>
//It defines the data structure of the data to be transmitted, so that it can also be used when receiving data in the application.
#include "mpu6050.h"
//The following are the addresses of a bunch of registers. Some are used to initialize the mpu6050, and some are used to read data.
//Anyway, in the end, i2c functions are used to read/write.
\t\t\t\t\t\t\t\t\t
#define SMPLRT_DIV 0x19 //Sampling frequency register-25 Typical value: 0x07 (125Hz)
//The data in the register set is updated according to the sampling frequency
#define CONFIG 0x1A //Configuration register-26-Typical value: 0x06 (5Hz)
//DLPF is disabled (DLPF_CFG=0 or 7)
#define GYRO_CONFIG 0x1B//Gyroscope configuration-27, you can configure self-test and full-scale range
//Typical value: 0x18 (no self-test, 2000deg/s)
#define ACCEL_CONFIG 0x1C //Acceleration configuration-28 can configure self-test, full-scale range and high-pass filter frequency
//Typical value: 0x01 (no self-test, 2G, 5Hz)
#define ACCEL_XOUT_H 0x3B //59-65, accelerometer measurement value XOUT_H
#define ACCEL_XOUT_L 0x3C // XOUT_L
#define ACCEL_YOUT_H 0x3D //YOUT_H
#define ACCEL_YOUT_L 0x3E //YOUT_L
#define ACCEL_ZOUT_H 0x3F //ZOUT_H
#define ACCEL_ZOUT_L 0x40 //ZOUT_L---64
#define TEMP_OUT_H 0x41 //Temperature measurement value--65
#define TEMP_OUT_L 0x42
#define GYRO_XOUT_H 0x43 //Gyroscope value--67, sampling frequency (defined by register 25) is written to these registers
#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 //Gyro value--72
#define PWR_MGMT_1 0x6B //Power management typical value: 0x00 (normally enabled)

//Design a global device object
struct mpu_sensor{
int dev_major;
struct device *dev;
struct class *cls;
struct i2c_client *client;//Record client in probe
};

struct mpu_sensor *mpu_dev;

//char buf5[2] = {ACCEL_CONFIG, 0x01};
//mpu6050_write_bytes(mpu_dev->client, buf5, 2);
//Encapsulates a function for i2c to write data, so that we only need to pass data to this function and it will call the i2c transfer function to write.
int mpu6050_write_bytes(struct i2c_client *client, char *buf, int count)
{

int ret;
struct i2c_adapter *adapter = client->adapter;
struct i2c_msg msg;

msg.addr = client->addr; //This is not a register address, but the address of the mpu6050 device, which is 0x68. After the match is successful,
                             //addr will be automatically found from the device tree
msg.flags = 0; //Indicates whether this i2c_transfer operation is reading or writing, 0 is writing, 1 is reading
msg.len = count; //Number of Bytes of data
msg.buf = buf; //data
\t
ret = i2c_transfer(adapter, & amp;msg, 1); //Send data and return how many B of data were actually transferred

return ret==1?count:ret;

}
//This is similar to the one above
int mpu6050_read_bytes(struct i2c_client *client, char *buf, int count)
{
\t
int ret;
struct i2c_adapter *adapter = client->adapter;
struct i2c_msg msg;
\t
msg.addr = client->addr;
msg.flags = I2C_M_RD; //This macro is actually 1. As mentioned earlier, it reads 1
msg.len = count;
msg.buf = buf;
\t\t
ret = i2c_transfer(adapter, & amp;msg, 1);//0 write 1 read
\t
return ret==1?count:ret;
}

//Read the address of a specific register and then return the value in this register
//To read data from i2c, you need to send it an address first, and then perform a read operation to read the data in the corresponding register.
int mpu6050_read_reg_byte(struct i2c_client *client, char reg)
{
//Write the address of the register first, then read the value of the register

int ret;
struct i2c_adapter *adapter = client->adapter;
struct i2c_msg msg[2];

char rxbuf[1];
\t
msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].len = 1;
msg[0].buf = & amp;reg;

msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;
msg[1].len = 1;
msg[1].buf = rxbuf;
\t\t
ret = i2c_transfer(adapter, msg, 2);
if(ret < 0)
{
printk("i2c_transfer read error\\
");
return ret;
}

return rxbuf[0];

}
//When the application opens the device node, it initializes the peripheral ~ a bunch of register writing operations
//For details on how to write, see the init part of the chip manual. Most of them follow this process.
int mpu6050_drv_open(struct inode *inode, struct file *filp)
{
char buf1[2] = {PWR_MGMT_1, 0x0};//Power management
mpu6050_write_bytes(mpu_dev->client, buf1, 2);

char buf2[2] = {SMPLRT_DIV, 0x07};//Sampling frequency register 125HZ
mpu6050_write_bytes(mpu_dev->client, buf2, 2);

char buf3[2] = {CONFIG, 0x06};//Configuration register typical value
mpu6050_write_bytes(mpu_dev->client, buf3, 2);

char buf4[2] ={GYRO_CONFIG, 0x18};//Gyroscope configuration
mpu6050_write_bytes(mpu_dev->client, buf4, 2);

char buf5[2] = {ACCEL_CONFIG, 0x01};//Acceleration configuration
mpu6050_write_bytes(mpu_dev->client, buf5, 2);

return 0;
}
int mpu6050_drv_close(struct inode *inode, struct file *filp)
{
return 0;
}
//This is to determine what the cmd passed by the application is, and then return the data requested above accordingly.
//This args parameter is very interesting. You can use a pointer to get the data into &args first, and then the application uses a pointer to connect the data.
//The data can be transferred
long mpu6050_drv_ioctl (struct file *filp, unsigned int cmd, unsigned long args)
{
union mpu6050_data data;

switch(cmd){
case IOC_GET_ACCEL:
//Read data. The data manual says that one sensor data is stored in two 8-bit registers, so we have to combine them.
            //For example, the following two together are the data of x Euler angle, and the others are similar
data.accel.x = mpu6050_read_reg_byte(mpu_dev->client, ACCEL_XOUT_L);
data.accel.x |= mpu6050_read_reg_byte(mpu_dev->client, ACCEL_XOUT_H) << 8;

data.accel.y = mpu6050_read_reg_byte(mpu_dev->client, ACCEL_YOUT_L);
data.accel.y |= mpu6050_read_reg_byte(mpu_dev->client, ACCEL_YOUT_H) << 8;

data.accel.z = mpu6050_read_reg_byte(mpu_dev->client, ACCEL_ZOUT_L);
data.accel.z |= mpu6050_read_reg_byte(mpu_dev->client, ACCEL_ZOUT_H) << 8;
break;
case IOC_GET_GYRO:
data.gyro.x = mpu6050_read_reg_byte(mpu_dev->client, GYRO_XOUT_L);
data.gyro.x |= mpu6050_read_reg_byte(mpu_dev->client, GYRO_XOUT_H) << 8;


data.gyro.y = mpu6050_read_reg_byte(mpu_dev->client, GYRO_YOUT_L);
data.gyro.y |= mpu6050_read_reg_byte(mpu_dev->client, GYRO_YOUT_H) << 8;

data.gyro.z= mpu6050_read_reg_byte(mpu_dev->client, GYRO_ZOUT_L);
data.gyro.z |= mpu6050_read_reg_byte(mpu_dev->client, GYRO_ZOUT_H) << 8;
break;
case IOC_GET_TEMP:
data.temp = mpu6050_read_reg_byte(mpu_dev->client, TEMP_OUT_L);
data.temp |= mpu6050_read_reg_byte(mpu_dev->client, TEMP_OUT_H) << 8;
break;
default:
printk("invalid cmd\\
");
return -EINVAL;
}
    //Why use switch here? Because the data structure that stores data is a union
//Give all data to the user
if(copy_to_user((void __user * )args, & amp;data, sizeof(data)) > 0)
return -EFAULT;
return 0;
}
//The word "fuck"
const struct file_operations mpu6050_fops = {
.open = mpu6050_drv_open,
.release = mpu6050_drv_close,
.unlocked_ioctl = mpu6050_drv_ioctl,

};

//The match is successful, proceed with the character device driver process
int mpu6050_drv_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
printk("-----%s----\\
", __FUNCTION__);

/*
Apply for a device number and implement fops
Create device file
Initialize the i2c slave device through the i2c interface
*/

mpu_dev = kzalloc(sizeof(struct mpu_sensor), GFP_KERNEL);
\t
mpu_dev->client = client;

mpu_dev->dev_major = register_chrdev(0,"mpu_drv", & amp;mpu6050_fops);

mpu_dev->cls = class_create(THIS_MODULE, "mpu_cls");

mpu_dev->dev = device_create(mpu_dev->cls, NULL, MKDEV(mpu_dev->dev_major, 0),
NULL, "mpu_sensor");
\t

\t
return 0;

}


int mpu5060_drv_remove(struct i2c_client *client)
{
printk("-----%s----\\
", __FUNCTION__);
device_destroy(mpu_dev->cls, MKDEV(mpu_dev->dev_major, 0));
class_destroy(mpu_dev->cls);
unregister_chrdev(mpu_dev->dev_major, "mpu_drv");
kfree(mpu_dev);
return 0;
}

//Used to match the device tree
const struct of_device_id of_mpu6050_id[] = {
{
.compatible = "lch-mpu6050",
},
{/*northing to be done*/},

};

//Matching for traditional methods
const struct i2c_device_id mpu_id_table[] = {
{"mpu6050_drv", 0x1111},
{/*northing to be done*/},
};
\t
struct i2c_driver mpu6050_drv = {
.probe = mpu6050_drv_probe,
.remove = mpu5060_drv_remove,
.driver = {
.name = "lch-mpu6050",//Write whatever you want, /sys/bus/i2c/driver/mpu6050_drv
.of_match_table = of_match_ptr(of_mpu6050_id),
},
\t
.id_table = mpu_id_table,//Matching in non-device tree situations, no need to use it in device tree mode
};


static int __init mpu6050_drv_init(void)
{
// 1. Build the i2c driver and register it to the i2c bus
return i2c_add_driver( & amp;mpu6050_drv);

}

static void __exit mpu6050_drv_exit(void)
{
i2c_del_driver( & amp;mpu6050_drv);

}

module_init(mpu6050_drv_init);
module_exit(mpu6050_drv_exit);
MODULE_LICENSE("GPL");

app_i2c.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>


#include "mpu6050.h"


int main(int argc, char *argv[])
{
int fd;
\t
union mpu6050_data data;
\t
fd = open("/dev/mpu_sensor", O_RDWR);
if(fd < 0)
{
perror("open");
exit(1);
}

while(1)
{
ioctl(fd, IOC_GET_ACCEL, & amp;data);
printf("accel data : x = %d, y=%d, z=%d\\
", data.accel.x, data.accel.y, data.accel.z);

\t
ioctl(fd, IOC_GET_GYRO, & amp;data);
printf("gyro data : x = %d, y=%d, z=%d\\
", data.gyro.x, data.gyro.y, data.gyro.z);

sleep(1);

}

close(fd);
\t

return 0;

}

Insmod the driver into the system and run the application and the information will be printed out.

Thinking:

It is always said that “the driver is the mechanism and the application is the strategy”, but this time when writing the i2c device, I feel that there is too much “strategy” code in the driver. If read and write are used to form an interface for reading and writing i2c registers, then it will be exposed in this way. The i2c device node can be a driver that only focuses on the i2c protocol, regardless of the specific i2c interface peripherals. A lot of code about the mpu6050 can be written in the application. Moreover, this device node can also be used by other i2c interface peripherals.